@sveltia/ui 0.62.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/components/button/button.svelte +6 -1
  2. package/dist/components/resizable-pane/resizable-handle.svelte +12 -3
  3. package/dist/components/select/select-tags.svelte +8 -1
  4. package/dist/components/text-editor/constants.d.ts +0 -1
  5. package/dist/components/text-editor/constants.js +1 -36
  6. package/dist/components/text-editor/core.js +24 -31
  7. package/dist/components/text-editor/lexical-root.svelte +0 -68
  8. package/dist/components/text-editor/shiki/cache.d.ts +3 -0
  9. package/dist/components/text-editor/shiki/cache.js +121 -0
  10. package/dist/components/text-editor/shiki/engine-entry.d.ts +2 -0
  11. package/dist/components/text-editor/shiki/engine-entry.js +20 -0
  12. package/dist/components/text-editor/shiki/facade.d.ts +16 -0
  13. package/dist/components/text-editor/shiki/facade.js +452 -0
  14. package/dist/components/text-editor/shiki/generated.d.ts +28 -0
  15. package/dist/components/text-editor/shiki/generated.js +25 -0
  16. package/dist/components/text-editor/shiki/highlighter.d.ts +11 -0
  17. package/dist/components/text-editor/shiki/highlighter.js +477 -0
  18. package/dist/components/text-editor/shiki/loader.d.ts +6 -0
  19. package/dist/components/text-editor/shiki/loader.js +102 -0
  20. package/dist/components/text-editor/shiki/theme.d.ts +12 -0
  21. package/dist/components/text-editor/shiki/theme.js +86 -0
  22. package/dist/components/text-editor/toolbar/code-language-switcher.svelte +24 -34
  23. package/dist/components/text-editor/toolbar/toggle-block-menu-item.svelte +1 -5
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +4 -0
  26. package/dist/services/group.svelte.d.ts +2 -1
  27. package/dist/services/group.svelte.js +183 -50
  28. package/dist/services/tree.svelte.d.ts +8 -6
  29. package/dist/services/tree.svelte.js +104 -45
  30. package/dist/shiki-engine.js +152 -0
  31. package/dist/typedefs.d.ts +52 -0
  32. package/dist/typedefs.js +29 -0
  33. package/package.json +13 -11
@@ -16,10 +16,6 @@ const TYPE_AHEAD_TIMEOUT = 500;
16
16
  * CSS selector to retrieve the tree items.
17
17
  */
18
18
  const ITEM_SELECTOR = '[role="treeitem"]';
19
- /**
20
- * CSS selector to retrieve the tree item containers, including the widget root.
21
- */
22
- const GROUP_SELECTOR = '[role="group"], [role="tree"]';
23
19
 
24
20
  /**
25
21
  * Implement keyboard and mouse interactions for the `tree` composite widget, following the ARIA
@@ -164,9 +160,48 @@ export class Tree {
164
160
  * @type {HTMLElement[]}
165
161
  */
166
162
  get visibleItems() {
167
- return this.allItems.filter(
168
- (item) => !item.matches('[hidden], [aria-hidden="true"]') && !this.hasCollapsedAncestor(item),
169
- );
163
+ /** @type {HTMLElement[]} */
164
+ const items = [];
165
+
166
+ /**
167
+ * Collect the items below the given node in document order, descending only into the parents
168
+ * that are actually expanded. Pruning the collapsed subtrees outright is what keeps this
169
+ * proportional to the number of displayed items; testing each item in the tree for a collapsed
170
+ * ancestor instead costs a walk back up per item.
171
+ * @param {Element} node Node whose children to scan.
172
+ */
173
+ const collect = (node) => {
174
+ [...node.children].forEach((child) => {
175
+ if (child.getAttribute('role') !== 'treeitem') {
176
+ // Anything else is a wrapper to see through — including a group that no item owns, whose
177
+ // items sit at the level of the group itself
178
+ collect(child);
179
+
180
+ return;
181
+ }
182
+
183
+ const item = /** @type {HTMLElement} */ (child);
184
+
185
+ if (!item.matches('[hidden], [aria-hidden="true"]')) {
186
+ items.push(item);
187
+ }
188
+
189
+ // A collapsed parent hides everything below it, so there is nothing to descend into
190
+ if (this.isParent(item) && !this.isExpanded(item)) {
191
+ return;
192
+ }
193
+
194
+ const group = this.getGroup(item);
195
+
196
+ if (group) {
197
+ collect(group);
198
+ }
199
+ });
200
+ };
201
+
202
+ collect(this.parent);
203
+
204
+ return items;
170
205
  }
171
206
 
172
207
  /**
@@ -182,7 +217,9 @@ export class Tree {
182
217
  * @type {HTMLElement[]}
183
218
  */
184
219
  get selectedItems() {
185
- return this.allItems.filter((item) => item.matches('[aria-selected="true"]'));
220
+ return /** @type {HTMLElement[]} */ ([
221
+ ...this.parent.querySelectorAll(`${ITEM_SELECTOR}[aria-selected="true"]`),
222
+ ]);
186
223
  }
187
224
 
188
225
  /**
@@ -221,9 +258,31 @@ export class Tree {
221
258
  * @returns {HTMLElement[]} Child items.
222
259
  */
223
260
  getItemsInGroup(group) {
224
- return /** @type {HTMLElement[]} */ ([...group.querySelectorAll(ITEM_SELECTOR)]).filter(
225
- (item) => item.parentElement?.closest(GROUP_SELECTOR) === group,
226
- );
261
+ /** @type {HTMLElement[]} */
262
+ const items = [];
263
+
264
+ /**
265
+ * Scan the children of the given node, descending through plain wrapper elements but never into
266
+ * an item or a nested group — whatever lies below those belongs to the item that owns them.
267
+ * Querying the whole subtree and then discarding the deeper items with a `closest()` call each
268
+ * costs `update()`, which runs this once per group, quadratic time overall.
269
+ * @param {Element} node Node whose children to scan.
270
+ */
271
+ const walk = (node) => {
272
+ [...node.children].forEach((child) => {
273
+ const role = child.getAttribute('role');
274
+
275
+ if (role === 'treeitem') {
276
+ items.push(/** @type {HTMLElement} */ (child));
277
+ } else if (role !== 'group' && role !== 'tree') {
278
+ walk(child);
279
+ }
280
+ });
281
+ };
282
+
283
+ walk(group);
284
+
285
+ return items;
227
286
  }
228
287
 
229
288
  /**
@@ -255,25 +314,6 @@ export class Tree {
255
314
  return item.getAttribute('aria-expanded') === 'true';
256
315
  }
257
316
 
258
- /**
259
- * Whether any of the ancestors of the given item is collapsed, meaning the item is not displayed.
260
- * @param {HTMLElement} item Item.
261
- * @returns {boolean} Result.
262
- */
263
- hasCollapsedAncestor(item) {
264
- let ancestor = this.getParentItem(item);
265
-
266
- while (ancestor) {
267
- if (!this.isExpanded(ancestor)) {
268
- return true;
269
- }
270
-
271
- ancestor = this.getParentItem(ancestor);
272
- }
273
-
274
- return false;
275
- }
276
-
277
317
  /**
278
318
  * Get the text label of the given item, which is used for the type-ahead search.
279
319
  * @param {HTMLElement} item Item.
@@ -316,12 +356,10 @@ export class Tree {
316
356
  const current =
317
357
  activeItems.find((item) => item === document.activeElement) ??
318
358
  activeItems.find((item) => item.tabIndex === 0) ??
319
- activeItems.find((item) => item.matches('[aria-selected="true"]')) ??
359
+ activeItems.find((item) => item.getAttribute('aria-selected') === 'true') ??
320
360
  activeItems[0];
321
361
 
322
- allItems.forEach((item) => {
323
- item.tabIndex = item === current ? 0 : -1;
324
- });
362
+ this.setTabStop(current);
325
363
  }
326
364
 
327
365
  /**
@@ -336,6 +374,27 @@ export class Tree {
336
374
  }
337
375
  }
338
376
 
377
+ /**
378
+ * Put exactly one item in the tab order. Only the items actually in it are touched: writing a
379
+ * `tabindex` to every item costs a pass over the whole widget on each arrow key, and all but one
380
+ * of those writes set the value the item already had.
381
+ * @param {HTMLElement} [item] Item to become the tab stop. When omitted, no item is left in the
382
+ * tab order, which is the case for a tree with nothing to focus.
383
+ */
384
+ setTabStop(item) {
385
+ this.parent
386
+ .querySelectorAll(`${ITEM_SELECTOR}[tabindex]:not([tabindex="-1"])`)
387
+ .forEach((element) => {
388
+ if (element !== item) {
389
+ /** @type {HTMLElement} */ (element).tabIndex = -1;
390
+ }
391
+ });
392
+
393
+ if (item) {
394
+ item.tabIndex = 0;
395
+ }
396
+ }
397
+
339
398
  /**
340
399
  * Move focus to the given item.
341
400
  * @param {HTMLElement} item Item to be focused.
@@ -344,10 +403,7 @@ export class Tree {
344
403
  * `selectionFollowsFocus` option.
345
404
  */
346
405
  focusItem(item, { select = this.selectionFollowsFocus } = {}) {
347
- this.allItems.forEach((element) => {
348
- element.tabIndex = element === item ? 0 : -1;
349
- });
350
-
406
+ this.setTabStop(item);
351
407
  item.focus();
352
408
  item.dispatchEvent(new CustomEvent('Focus'));
353
409
  this.scrollIntoView(item);
@@ -363,7 +419,7 @@ export class Tree {
363
419
  * @param {boolean} selected Whether to select the item.
364
420
  */
365
421
  setSelected(item, selected) {
366
- if (item.matches('[aria-selected="true"]') === selected) {
422
+ if ((item.getAttribute('aria-selected') === 'true') === selected) {
367
423
  return;
368
424
  }
369
425
 
@@ -402,13 +458,18 @@ export class Tree {
402
458
  );
403
459
  });
404
460
  } else if (multi && additive) {
405
- this.setSelected(item, !item.matches('[aria-selected="true"]'));
461
+ this.setSelected(item, item.getAttribute('aria-selected') !== 'true');
406
462
  this.anchor = item;
407
463
  } else {
408
- this.allItems.forEach((element) => {
409
- this.setSelected(element, element === item);
464
+ // Only the items that are actually selected need clearing; running every item in the tree
465
+ // through `setSelected()` would leave all but one of them untouched anyway
466
+ this.selectedItems.forEach((element) => {
467
+ if (element !== item) {
468
+ this.setSelected(element, false);
469
+ }
410
470
  });
411
471
 
472
+ this.setSelected(item, true);
412
473
  this.anchor = item;
413
474
  }
414
475
 
@@ -517,9 +578,7 @@ export class Tree {
517
578
  return;
518
579
  }
519
580
 
520
- this.allItems.forEach((element) => {
521
- element.tabIndex = element === item ? 0 : -1;
522
- });
581
+ this.setTabStop(item);
523
582
  }
524
583
 
525
584
  /**