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.