PageSourceSearch

https://lemonadejs.com/v6/toolbar.js

js lemonadejs.com collected 2026-09-25 22:12:14 UTC 11,350 bytes, 290 lines download raw bytes

1// components/toolbar/src/index.ts
2import { component, html } from "lemonadejs";
3import Contextmenu from "@lemonadejs/contextmenu";
4import Color from "@lemonadejs/color";
5var Toolbar = component("toolbar", {
6  options: Array,
7  // ToolbarItem[]
8  position: "",
9  // '' = fixed bottom bar (v5 default) | 'static' | 'left'
10  visible: true,
11  // false hides the whole bar
12  onitemclick: Function,
13  // (e, item, index) on any item activation
14  onchange: Function,
15  // (e, item, option) when a picker option is chosen, (e, item, { value }) on a color pick
16  api: { open: Function, close: Function, refresh: Function }
17}, (props, { state, listen }) => {
18  const expanded = state(null);
19  const version = state(0);
20  const colorAt = state(null);
21  let root = null;
22  let menu = null;
23  const items = () => props.options.value || [];
24  const live = (fn) => () => {
25    void version.value;
26    return fn();
27  };
28  const headerEl = (index) => {
29    const list = items();
30    let at = 0;
31    for (let i = 0; i < index; i++) {
32      if (list[i] && list[i].type === "select") {
33        at++;
34      }
35    }
36    return root?.querySelectorAll(".lm-toolbar-picker-header")[at] || null;
37  };
38  const cancel = (e) => {
39    e.preventDefault();
40    e.stopImmediatePropagation();
41  };
42  const normalize = (item) => (item.options || []).map((o) => {
43    const option = typeof o === "string" ? { title: o } : { ...o };
44    const inner = option.onclick;
45    option.onclick = (e, picked) => {
46      inner?.(e, picked);
47      props.onchange?.(e, item, picked);
48      if (picked.title) {
49        item.title = picked.title;
50        version.value = version.value + 1;
51      }
52    };
53    return option;
54  });
55  const open = (index, e) => {
56    const item = items()[index];
57    if (!item || item.type !== "select" || item.disabled) {
58      return;
59    }
60    if (expanded.value === index) {
61      return;
62    }
63    const el = e?.currentTarget || headerEl(index);
64    const rect = el?.getBoundingClientRect();
65    menu?.open(normalize(item), rect ? rect.left : 0, rect ? rect.bottom + 2 : 0);
66    expanded.value = index;
67  };
68  const press = (e, index) => {
69    cancel(e);
70    open(index, e);
71  };
72  const activate = (e, index) => {
73    const item = items()[index];
74    if (!item || item.disabled) {
75      e.preventDefault();
76      return;
77    }
78    item.onclick?.(e, item);
79    props.onitemclick?.(e, item, index);
80  };
81  props.ref?.({
82    /** Open the picker dropdown of the select item at items index */
83    open: (index) => open(index),
84    close: () => menu?.close(),
85    /** Re-evaluate the live item bindings after in-place mutations */
86    refresh: () => {
87      version.value = version.value + 1;
88    }
89  });
90  const pickColor = (e, index) => {
91    const item = items()[index];
92    if (!item || item.disabled) {
93      e.preventDefault();
94      return;
95    }
96    item.onclick?.(e, item);
97    props.onitemclick?.(e, item, index);
98    if (colorAt.value && colorAt.value.index === index) {
99      colorAt.value = null;
100      return;
101    }
102    const host = e.currentTarget.closest(".lm-toolbar-item");
103    const rect = host?.getBoundingClientRect();
104    colorAt.value = { index, left: rect ? rect.left : 0, top: rect ? rect.bottom + 4 : 0 };
105  };
106  const colorPicked = (value, index) => {
107    const item = items()[index];
108    if (!item) {
109      return;
110    }
111    item.value = value;
112    version.value = version.value + 1;
113    item.onchange?.(value, item);
114    props.onchange?.(null, item, { value });
115    colorAt.value = null;
116  };
117  listen(document, "mousedown", (e) => {
118    if (colorAt.value && !e.target?.closest?.(".lm-toolbar-color-pop, .lm-toolbar-color")) {
119      colorAt.value = null;
120    }
121  });
122  listen(document, "keydown", (e) => {
123    if (e.key === "Escape" && colorAt.value) {
124      const at = colorAt.value.index;
125      colorAt.value = null;
126      focusItem(at);
127    }
128  });
129  const interactive = (it) => !!it && it.type !== "divisor" && it.type !== "divider" && !it.disabled && it.visible !== false;
130  const firstOk = () => {
131    const list = items();
132    for (let i = 0; i < list.length; i++) {
133      if (interactive(list[i])) {
134        return i;
135      }
136    }
137    return 0;
138  };
139  const rov = state(firstOk());
140  const ordinal = (index) => {
141    const list = items();
142    let n = 0;
143    for (let i = 0; i < index; i++) {
144      const t = list[i]?.type;
145      if (t !== "divisor" && t !== "divider") {
146        n++;
147      }
148    }
149    return n;
150  };
151  const focusItem = (index) => {
152    rov.value = index;
153    const el = root?.querySelectorAll(
154      ".lm-toolbar-item > a, .lm-toolbar-picker-header"
155    )[ordinal(index)];
156    el?.focus();
157  };
158  const onBarKey = (e) => {
159    const vertical = props.position.value === "left";
160    const forward = vertical ? "ArrowDown" : "ArrowRight";
161    const backward = vertical ? "ArrowUp" : "ArrowLeft";
162    if (e.key !== forward && e.key !== backward && e.key !== "Home" && e.key !== "End") {
163      return;
164    }
165    const idxs = items().map((it, i) => interactive(it) ? i : -1).filter((i) => i >= 0);
166    if (!idxs.length) {
167      return;
168    }
169    e.preventDefault();
170    const cur = idxs.indexOf(rov.value);
171    const at = e.key === "Home" ? 0 : e.key === "End" ? idxs.length - 1 : cur < 0 ? 0 : (cur + (e.key === forward ? 1 : -1) + idxs.length) % idxs.length;
172    focusItem(idxs[at]);
173  };
174  const roving = (index) => live(() => interactive(items()[index]) && rov.value === index ? "0" : "-1");
175  const itemView = (item, index) => {
176    if (item.type === "divisor" || item.type === "divider") {
177      return html`<div class="lm-toolbar-divisor" key="${item}" role="separator"></div>`;
178    }
179    if (item.type === "select") {
180      return html`<div class="lm-toolbar-picker" key="${item}"
181                data-selected="${live(() => item.selected ? "true" : false)}"
182                data-visible="${live(() => item.visible === void 0 ? false : String(item.visible))}"
183                data-disabled="${live(() => item.disabled ? "true" : false)}">
184                <div class="lm-toolbar-picker-header" role="button"
185                    style="${item.width ? "width:" + item.width + "px" : false}"
186                    tabindex="${roving(index)}"
187                    title="${item.tooltip || false}"
188                    aria-haspopup="true"
189                    aria-disabled="${live(() => item.disabled ? "true" : false)}"
190                    aria-expanded="${() => expanded.value === index ? "true" : "false"}"
191                    onfocus="${() => rov.value = index}"
192                    onmousedown="${(e) => press(e, index)}"
193                    onmouseover="${(e) => press(e, index)}"
194                    oncontextmenu="${(e) => press(e, index)}"
195                    onkeydown="${(e) => {
196        if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
197          press(e, index);
198        }
199      }}"><span class="lm-toolbar-picker-label">${live(() => item.title || "")}</span>${// No explicit width: an invisible zero-height stack
200      // of every option title sizes the header to the
201      // LONGEST one, so mirroring a pick into the label
202      // never reflows the rest of the toolbar
203      !item.width ? html`<span class="lm-toolbar-picker-sizer" aria-hidden="true">${(item.options || []).map(
204        (o) => html`<span>${typeof o === "string" ? o : o.title || ""}</span>`
205      )}</span>` : ""}</div>
206            </div>`;
207    }
208    if (item.type === "color") {
209      return html`<div class="lm-toolbar-item lm-toolbar-color" key="${item}"
210                data-visible="${live(() => item.visible === void 0 ? false : String(item.visible))}"
211                data-disabled="${live(() => item.disabled ? "true" : false)}">
212                <a title="${item.tooltip || item.title || false}" role="button"
213                    tabindex="${roving(index)}"
214                    aria-label="${item.tooltip || item.title || false}"
215                    aria-haspopup="true"
216                    aria-disabled="${live(() => item.disabled ? "true" : false)}"
217                    aria-expanded="${() => colorAt.value?.index === index ? "true" : "false"}"
218                    onfocus="${() => rov.value = index}"
219                    onclick="${(e) => pickColor(e, index)}"
220                    onkeydown="${(e) => {
221        if (e.key === "Enter" || e.key === " ") {
222          e.preventDefault();
223          pickColor(e, index);
224        }
225      }}">
226                    ${item.icon ? html`<i class="material-icons material-symbols-outlined" aria-hidden="true">${item.icon}</i>` : ""}
227                    <span class="lm-toolbar-swatch"
228                        style="${live(() => "background-color:" + (item.value || "transparent"))}"></span>
229                </a>
230            </div>`;
231    }
232    return html`<div class="lm-toolbar-item" key="${item}"
233            data-selected="${live(() => item.selected ? "true" : false)}"
234            data-visible="${live(() => item.visible === void 0 ? false : String(item.visible))}"
235            data-disabled="${live(() => item.disabled ? "true" : false)}"
236            data-gap="${item.gap ? "true" : false}">
237            <a href="${item.route || false}" title="${live(() => item.tooltip || item.title || false)}"
238                role="${item.route ? false : "button"}"
239                tabindex="${roving(index)}"
240                aria-label="${live(() => item.tooltip || item.title || false)}"
241                aria-disabled="${live(() => item.disabled ? "true" : false)}"
242                aria-pressed="${live(() => item.selected === void 0 ? false : String(!!item.selected))}"
243                onfocus="${() => rov.value = index}"
244                onclick="${(e) => activate(e, index)}"
245                onkeydown="${(e) => {
246      if ((e.key === "Enter" || e.key === " ") && !item.route) {
247        e.preventDefault();
248        activate(e, index);
249      }
250    }}">
251                ${item.image ? html`<img src="${item.image}" alt="" />` : ""}
252                ${item.icon ? html`<i class="material-icons material-symbols-outlined" aria-hidden="true">${live(() => item.icon)}</i>` : ""}
253                ${item.title ? html`<span>${live(() => item.title)}</span>` : ""}
254            </a>
255        </div>`;
256  };
257  return html`<div class="lm-toolbar" role="toolbar"
258        ref="${(el) => root = el}"
259        aria-orientation="${() => props.position.value === "left" ? "vertical" : false}"
260        data-position="${() => props.position.value || false}"
261        data-visible="${() => props.visible.value === false ? "false" : "true"}"
262        onkeydown="${onBarKey}">
263        ${() => items().map((item, i) => itemView(item, i))}
264        <${Contextmenu} ref="${(a) => menu = a}"
265            onclose="${() => {
266    const at = expanded.value;
267    expanded.value = null;
268    if (at !== null && root?.contains(document.activeElement)) {
269      headerEl(at)?.focus();
270    }
271  }}" />
272        ${() => {
273    const at = colorAt.value;
274    if (!at) {
275      return "";
276    }
277    const item = items()[at.index];
278    return html`<div class="lm-toolbar-color-pop"
279                style="${"left:" + at.left + "px;top:" + at.top + "px"}">
280                <${Color} type="inline" bind="${item && item.value || ""}"
281                    onchange="${(value) => colorPicked(value, at.index)}" />
282            </div>`;
283  }}
284    </div>`;
285});
286var index_default = Toolbar;
287export {
288  Toolbar,
289  index_default as default
290};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.