@hakumi-dev/hakumi-components 0.1.17-pre → 0.1.19-pre

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 (56) hide show
  1. package/README.md +218 -369
  2. package/app/javascript/hakumi_components/controllers/hakumi/admin_panel_controller.js +5 -7
  3. package/app/javascript/hakumi_components/controllers/hakumi/back_top_controller.js +1 -1
  4. package/app/javascript/hakumi_components/controllers/hakumi/button_controller.js +108 -2
  5. package/app/javascript/hakumi_components/controllers/hakumi/calendar_controller.js +183 -95
  6. package/app/javascript/hakumi_components/controllers/hakumi/color_picker_controller.js +23 -285
  7. package/app/javascript/hakumi_components/controllers/hakumi/date_picker_controller.js +274 -262
  8. package/app/javascript/hakumi_components/controllers/hakumi/float_button_group_controller.js +2 -2
  9. package/app/javascript/hakumi_components/controllers/hakumi/message_controller.js +4 -2
  10. package/app/javascript/hakumi_components/controllers/hakumi/modal_controller.js +119 -125
  11. package/app/javascript/hakumi_components/controllers/hakumi/table/editable.js +291 -0
  12. package/app/javascript/hakumi_components/controllers/hakumi/table_controller.js +166 -366
  13. package/app/javascript/hakumi_components/controllers/hakumi/tabs_controller.js +8 -2
  14. package/app/javascript/hakumi_components/controllers/hakumi/tag_controller.js +27 -25
  15. package/app/javascript/hakumi_components/controllers/hakumi/tag_group_controller.js +19 -18
  16. package/app/javascript/hakumi_components/controllers/hakumi/theme_controller.js +116 -117
  17. package/app/javascript/hakumi_components/controllers/hakumi/transfer_controller.js +17 -1
  18. package/app/javascript/hakumi_components/controllers/hakumi/tree_controller.js +363 -78
  19. package/app/javascript/hakumi_components/controllers/hakumi/typography_controller.js +3 -3
  20. package/app/javascript/hakumi_components/controllers/hakumi/upload_controller.js +320 -204
  21. package/app/javascript/hakumi_components/core/render_component.js +37 -11
  22. package/app/javascript/hakumi_components/utils/color_helper.js +262 -0
  23. package/app/javascript/stylesheets/_base.scss +9 -0
  24. package/app/javascript/stylesheets/_hakumi_components.scss +1 -0
  25. package/app/javascript/stylesheets/components/_breadcrumb.scss +2 -2
  26. package/app/javascript/stylesheets/components/_calendar.scss +13 -13
  27. package/app/javascript/stylesheets/components/_cascader.scss +5 -5
  28. package/app/javascript/stylesheets/components/_checkbox.scss +9 -11
  29. package/app/javascript/stylesheets/components/_color_picker.scss +11 -11
  30. package/app/javascript/stylesheets/components/_date_picker.scss +4 -4
  31. package/app/javascript/stylesheets/components/_descriptions.scss +2 -2
  32. package/app/javascript/stylesheets/components/_drawer.scss +3 -3
  33. package/app/javascript/stylesheets/components/_dropdown.scss +2 -2
  34. package/app/javascript/stylesheets/components/_flex.scss +1 -1
  35. package/app/javascript/stylesheets/components/_float_button.scss +5 -5
  36. package/app/javascript/stylesheets/components/_form_item.scss +92 -0
  37. package/app/javascript/stylesheets/components/_image.scss +15 -15
  38. package/app/javascript/stylesheets/components/_input.scss +23 -113
  39. package/app/javascript/stylesheets/components/_layout.scss +27 -26
  40. package/app/javascript/stylesheets/components/_menu.scss +15 -15
  41. package/app/javascript/stylesheets/components/_modal.scss +13 -13
  42. package/app/javascript/stylesheets/components/_notification.scss +3 -3
  43. package/app/javascript/stylesheets/components/_popover.scss +1 -1
  44. package/app/javascript/stylesheets/components/_segmented.scss +3 -3
  45. package/app/javascript/stylesheets/components/_select.scss +6 -6
  46. package/app/javascript/stylesheets/components/_slider.scss +1 -1
  47. package/app/javascript/stylesheets/components/_spin.scss +2 -2
  48. package/app/javascript/stylesheets/components/_steps.scss +10 -10
  49. package/app/javascript/stylesheets/components/_switch.scss +11 -10
  50. package/app/javascript/stylesheets/components/_table.scss +6 -6
  51. package/app/javascript/stylesheets/components/_tag.scss +2 -2
  52. package/app/javascript/stylesheets/components/_tooltip.scss +4 -4
  53. package/app/javascript/stylesheets/components/_tree_select.scss +3 -3
  54. package/app/javascript/stylesheets/components/_typography.scss +3 -3
  55. package/app/javascript/stylesheets/components/_upload.scss +4 -4
  56. package/package.json +2 -2
@@ -30,6 +30,8 @@ export default class extends RegistryController {
30
30
  this.sortables = new Map()
31
31
  this.searching = false
32
32
  this.previousExpandedKeys = null
33
+ this.typeaheadBuffer = ""
34
+ this.typeaheadTimeout = null
33
35
 
34
36
  this.expandedKeys = new Set(this.hasExpandedKeysValue ? this.expandedKeysValue : this.defaultExpandKeysValue)
35
37
  this.selectedKeys = new Set(this.hasSelectedKeysValue ? this.selectedKeysValue : this.defaultSelectedKeysValue)
@@ -40,6 +42,12 @@ export default class extends RegistryController {
40
42
  this.applyExpandedStates()
41
43
  this.applySelectedStates()
42
44
  this.applyCheckedStates(true)
45
+ this.syncRovingTabindex()
46
+
47
+ this.boundHandleKeydown = this.handleKeydown.bind(this)
48
+ this.boundHandleFocusin = this.handleFocusin.bind(this)
49
+ this.element.addEventListener("keydown", this.boundHandleKeydown)
50
+ this.element.addEventListener("focusin", this.boundHandleFocusin)
43
51
 
44
52
  if (this.draggableValue) {
45
53
  this.setupSortables()
@@ -47,7 +55,16 @@ export default class extends RegistryController {
47
55
  }
48
56
 
49
57
  teardown() {
58
+ if (this.boundHandleKeydown) {
59
+ this.element.removeEventListener("keydown", this.boundHandleKeydown)
60
+ this.boundHandleKeydown = null
61
+ }
62
+ if (this.boundHandleFocusin) {
63
+ this.element.removeEventListener("focusin", this.boundHandleFocusin)
64
+ this.boundHandleFocusin = null
65
+ }
50
66
  this.destroySortables()
67
+ this.clearTypeahead()
51
68
  }
52
69
 
53
70
  disconnect() {
@@ -199,6 +216,7 @@ export default class extends RegistryController {
199
216
  }
200
217
 
201
218
  this.applyExpandedStates()
219
+ this.syncRovingTabindex()
202
220
  this.dispatch("expand", { detail: { expandedKeys: Array.from(this.expandedKeys), key } })
203
221
  }
204
222
 
@@ -296,6 +314,7 @@ export default class extends RegistryController {
296
314
  setExpandedKeys(keys) {
297
315
  this.expandedKeys = new Set(keys || [])
298
316
  this.applyExpandedStates()
317
+ this.syncRovingTabindex()
299
318
  }
300
319
 
301
320
  expandAll() {
@@ -305,11 +324,13 @@ export default class extends RegistryController {
305
324
  }
306
325
  })
307
326
  this.applyExpandedStates()
327
+ this.syncRovingTabindex()
308
328
  }
309
329
 
310
330
  collapseAll() {
311
331
  this.expandedKeys.clear()
312
332
  this.applyExpandedStates()
333
+ this.syncRovingTabindex()
313
334
  }
314
335
 
315
336
  getDescendants(key) {
@@ -331,61 +352,71 @@ export default class extends RegistryController {
331
352
 
332
353
  maybeLoadAsync(key) {
333
354
  const node = this.nodeMap.get(key)
334
- if (!node) return
335
-
336
- const isAsync = node.element.dataset.async === "true"
337
- const hasChildren = node.element.querySelector(".hakumi-tree-children")
338
-
339
- if (!isAsync || hasChildren) return
355
+ if (!this.shouldLoadAsyncNode(node)) return
340
356
 
341
357
  this.setLoading(key, true)
342
358
  this.dispatch("load", { detail: { key, node: node.element } })
343
359
  }
344
360
 
361
+ shouldLoadAsyncNode(node) {
362
+ if (!node) return false
363
+
364
+ return node.element.dataset.async === "true" && !node.element.querySelector(".hakumi-tree-children")
365
+ }
366
+
345
367
  setLoading(key, loading) {
346
368
  const node = this.nodeMap.get(key)
347
369
  if (!node) return
348
370
 
349
- node.element.dataset.loading = loading ? "true" : "false"
350
- node.element.classList.toggle("hakumi-tree-node-loading", loading)
351
-
352
-
353
371
  if (loading) {
354
372
  this.setError(key, false)
355
373
  }
356
374
 
375
+ this.applyLoadingState(node.element, loading)
376
+ this.syncLoadingSwitcher(node.element, loading)
377
+ }
357
378
 
358
- const switcher = node.element.querySelector(".hakumi-tree-switcher")
379
+ applyLoadingState(element, loading) {
380
+ element.dataset.loading = loading ? "true" : "false"
381
+ element.classList.toggle("hakumi-tree-node-loading", loading)
382
+ }
383
+
384
+ syncLoadingSwitcher(element, loading) {
385
+ const switcher = element.querySelector(".hakumi-tree-switcher")
359
386
  if (!switcher) return
360
387
 
361
388
  if (loading) {
362
-
363
- const switcherIcon = switcher.querySelector(".hakumi-tree-switcher-icon")
364
- if (switcherIcon) {
365
- switcherIcon.style.display = "none"
366
- }
367
-
368
-
369
- if (!switcher.querySelector(".hakumi-tree-loading-icon")) {
370
- const loadingIcon = document.createElement("span")
371
- loadingIcon.className = "hakumi-tree-loading-icon"
372
- loadingIcon.innerHTML = `<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
373
- <path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"/>
374
- </svg>`
375
- switcher.appendChild(loadingIcon)
376
- }
389
+ this.showLoadingSwitcher(switcher)
377
390
  } else {
391
+ this.hideLoadingSwitcher(switcher)
392
+ }
393
+ }
378
394
 
379
- const switcherIcon = switcher.querySelector(".hakumi-tree-switcher-icon")
380
- if (switcherIcon) {
381
- switcherIcon.style.display = ""
382
- }
395
+ showLoadingSwitcher(switcher) {
396
+ const switcherIcon = switcher.querySelector(".hakumi-tree-switcher-icon")
397
+ if (switcherIcon) {
398
+ switcherIcon.style.display = "none"
399
+ }
383
400
 
401
+ if (!switcher.querySelector(".hakumi-tree-loading-icon")) {
402
+ const loadingIcon = document.createElement("span")
403
+ loadingIcon.className = "hakumi-tree-loading-icon"
404
+ loadingIcon.innerHTML = `<svg viewBox="0 0 1024 1024" width="1em" height="1em" fill="currentColor">
405
+ <path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"/>
406
+ </svg>`
407
+ switcher.appendChild(loadingIcon)
408
+ }
409
+ }
384
410
 
385
- const loadingIcon = switcher.querySelector(".hakumi-tree-loading-icon")
386
- if (loadingIcon) {
387
- loadingIcon.remove()
388
- }
411
+ hideLoadingSwitcher(switcher) {
412
+ const switcherIcon = switcher.querySelector(".hakumi-tree-switcher-icon")
413
+ if (switcherIcon) {
414
+ switcherIcon.style.display = ""
415
+ }
416
+
417
+ const loadingIcon = switcher.querySelector(".hakumi-tree-loading-icon")
418
+ if (loadingIcon) {
419
+ loadingIcon.remove()
389
420
  }
390
421
  }
391
422
 
@@ -420,7 +451,6 @@ export default class extends RegistryController {
420
451
  }
421
452
 
422
453
  addNodes(parentKey, nodes) {
423
-
424
454
  if (!nodes || nodes.length === 0) {
425
455
  this.markAsLeaf(parentKey)
426
456
  this.setLoading(parentKey, false)
@@ -430,50 +460,58 @@ export default class extends RegistryController {
430
460
  const parent = this.nodeMap.get(parentKey)
431
461
  if (!parent) return
432
462
 
433
- if (parent.element.dataset.leaf === "true") {
434
- parent.element.dataset.leaf = "false"
435
- parent.element.classList.remove("hakumi-tree-node-leaf")
436
- const switcher = parent.element.querySelector(".hakumi-tree-switcher")
437
- if (switcher) {
438
- switcher.classList.remove("hakumi-tree-switcher-leaf")
439
- switcher.dataset.action = "click->hakumi--tree#toggle"
440
- if (switcher && !switcher.querySelector(".hakumi-tree-switcher-icon")) {
441
-
442
-
443
-
444
-
445
-
463
+ this.ensureAsyncParentCanContainChildren(parent)
464
+ const container = this.ensureChildrenContainer(parent.element)
465
+ this.appendNodeElements(container, nodes, parent, parentKey)
466
+ this.refreshAfterNodesAdded()
467
+ }
446
468
 
469
+ ensureAsyncParentCanContainChildren(parent) {
470
+ if (parent.element.dataset.leaf !== "true") return
447
471
 
472
+ parent.element.dataset.leaf = "false"
473
+ parent.element.classList.remove("hakumi-tree-node-leaf")
474
+ const switcher = parent.element.querySelector(".hakumi-tree-switcher")
475
+ if (!switcher) return
448
476
 
449
- const tmpl = this.nodeTemplateTarget.content.cloneNode(true)
450
- const tmplSwitcher = tmpl.querySelector(".hakumi-tree-switcher")
451
- const tmplIcon = tmplSwitcher.querySelector(".hakumi-tree-switcher-icon")
452
- if (tmplIcon) switcher.appendChild(tmplIcon.cloneNode(true))
453
- }
454
- }
477
+ switcher.classList.remove("hakumi-tree-switcher-leaf")
478
+ switcher.dataset.action = "click->hakumi--tree#toggle"
479
+ if (!switcher.querySelector(".hakumi-tree-switcher-icon")) {
480
+ const tmpl = this.nodeTemplateTarget.content.cloneNode(true)
481
+ const tmplSwitcher = tmpl.querySelector(".hakumi-tree-switcher")
482
+ const tmplIcon = tmplSwitcher.querySelector(".hakumi-tree-switcher-icon")
483
+ if (tmplIcon) switcher.appendChild(tmplIcon.cloneNode(true))
455
484
  }
485
+ }
456
486
 
457
- let container = parent.element.querySelector(".hakumi-tree-children")
487
+ ensureChildrenContainer(parentElement) {
488
+ let container = parentElement.querySelector(".hakumi-tree-children")
458
489
  if (!container) {
459
490
  container = document.createElement("div")
460
491
  container.className = "hakumi-tree-children"
461
492
  container.setAttribute("role", "group")
462
493
  container.dataset.hakumiTreeTarget = "children"
463
- parent.element.appendChild(container)
494
+ parentElement.appendChild(container)
464
495
  }
465
496
 
497
+ return container
498
+ }
499
+
500
+ appendNodeElements(container, nodes, parent, parentKey) {
466
501
  const parentLevel = parseInt(parent.element.dataset.level || "0", 10)
467
502
  nodes.forEach((node, index) => {
468
503
  const hasNext = index < nodes.length - 1
469
504
  const nodeElement = this.createNodeElement(node, parentLevel + 1, parentKey, hasNext)
470
505
  container.appendChild(nodeElement)
471
506
  })
507
+ }
472
508
 
509
+ refreshAfterNodesAdded() {
473
510
  this.buildNodeMap()
474
511
  this.applyExpandedStates()
475
512
  this.applySelectedStates()
476
513
  this.applyCheckedStates(true)
514
+ this.syncRovingTabindex()
477
515
  if (this.draggableValue) this.setupSortables()
478
516
  }
479
517
 
@@ -588,13 +626,7 @@ export default class extends RegistryController {
588
626
  setupSortables() {
589
627
  this.destroySortables()
590
628
 
591
- const containers = new Set()
592
- if (this.hasListTarget) {
593
- containers.add(this.listTarget)
594
- }
595
- this.childrenTargets.forEach((child) => containers.add(child))
596
-
597
- containers.forEach((container) => {
629
+ this.sortableContainers().forEach((container) => {
598
630
  const sortable = Sortable.create(container, {
599
631
  animation: 150,
600
632
  group: "hakumi-tree",
@@ -603,27 +635,43 @@ export default class extends RegistryController {
603
635
  ghostClass: "hakumi-tree-node-ghost",
604
636
  chosenClass: "hakumi-tree-node-chosen",
605
637
  dragClass: "hakumi-tree-node-drag",
606
- onEnd: (evt) => {
607
- const item = evt.item
608
- const parentNode = item.closest(".hakumi-tree-node")
609
- const parentKey = parentNode ? parentNode.dataset.key : null
610
- item.dataset.parentKey = parentKey || ""
611
- this.buildNodeMap()
612
- this.dispatch("dragEnd", {
613
- detail: {
614
- key: item.dataset.key,
615
- parentKey,
616
- oldIndex: evt.oldIndex,
617
- newIndex: evt.newIndex
618
- }
619
- })
620
- }
638
+ onEnd: (evt) => this.handleDragEnd(evt)
621
639
  })
622
640
 
623
641
  this.sortables.set(container, sortable)
624
642
  })
625
643
  }
626
644
 
645
+ sortableContainers() {
646
+ const containers = new Set()
647
+ if (this.hasListTarget) {
648
+ containers.add(this.listTarget)
649
+ }
650
+ this.childrenTargets.forEach((child) => containers.add(child))
651
+ return containers
652
+ }
653
+
654
+ handleDragEnd(event) {
655
+ const item = event.item
656
+ const parentKey = this.dragParentKey(event, item)
657
+ item.dataset.parentKey = parentKey || ""
658
+ this.buildNodeMap()
659
+ this.dispatch("dragEnd", {
660
+ detail: {
661
+ key: item.dataset.key,
662
+ parentKey,
663
+ oldIndex: event.oldIndex,
664
+ newIndex: event.newIndex
665
+ }
666
+ })
667
+ }
668
+
669
+ dragParentKey(event, item) {
670
+ const parentContainer = event.to || item.parentElement
671
+ const parentNode = parentContainer ? parentContainer.closest(".hakumi-tree-node") : null
672
+ return parentNode ? parentNode.dataset.key : null
673
+ }
674
+
627
675
  destroySortables() {
628
676
  this.sortables.forEach((sortable) => sortable.destroy())
629
677
  this.sortables.clear()
@@ -636,6 +684,7 @@ export default class extends RegistryController {
636
684
 
637
685
  applySearch(query) {
638
686
  const term = query.trim().toLowerCase()
687
+ this.clearTypeahead()
639
688
 
640
689
  if (!term) {
641
690
  if (this.searching && this.previousExpandedKeys) {
@@ -649,6 +698,7 @@ export default class extends RegistryController {
649
698
  if (titleEl) titleEl.textContent = titleEl.dataset.title || ""
650
699
  })
651
700
  this.applyExpandedStates()
701
+ this.syncRovingTabindex()
652
702
  return
653
703
  }
654
704
 
@@ -698,9 +748,244 @@ export default class extends RegistryController {
698
748
  })
699
749
 
700
750
  this.applyExpandedStates()
751
+ this.syncRovingTabindex()
701
752
  }
702
753
 
703
754
  closestNode(event) {
704
755
  return event.currentTarget.closest(".hakumi-tree-node")
705
756
  }
757
+
758
+ handleFocusin(event) {
759
+ const node = event.target.closest(".hakumi-tree-node")
760
+ if (!node || !this.element.contains(node) || !this.isVisibleNode(node)) return
761
+
762
+ this.setFocusedNode(node, { focus: false })
763
+ }
764
+
765
+ handleKeydown(event) {
766
+ const currentNode = event.target.closest(".hakumi-tree-node")
767
+ if (!currentNode || !this.element.contains(currentNode)) return
768
+
769
+ if (!["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft", "Home", "End", "Enter", " "].includes(event.key) && !this.isTypeaheadKey(event)) return
770
+
771
+ if (event.key === "ArrowDown") {
772
+ event.preventDefault()
773
+ this.focusRelativeNode(currentNode, 1)
774
+ return
775
+ }
776
+
777
+ if (event.key === "ArrowUp") {
778
+ event.preventDefault()
779
+ this.focusRelativeNode(currentNode, -1)
780
+ return
781
+ }
782
+
783
+ if (event.key === "Home") {
784
+ event.preventDefault()
785
+ this.focusFirstVisibleNode()
786
+ return
787
+ }
788
+
789
+ if (event.key === "End") {
790
+ event.preventDefault()
791
+ this.focusLastVisibleNode()
792
+ return
793
+ }
794
+
795
+ if (event.key === "ArrowRight") {
796
+ event.preventDefault()
797
+ this.expandOrFocusChild(currentNode)
798
+ return
799
+ }
800
+
801
+ if (event.key === "ArrowLeft") {
802
+ event.preventDefault()
803
+ this.collapseOrFocusParent(currentNode)
804
+ return
805
+ }
806
+
807
+ if (this.isTypeaheadKey(event)) {
808
+ event.preventDefault()
809
+ this.focusTypeaheadMatch(currentNode, event.key)
810
+ return
811
+ }
812
+
813
+ event.preventDefault()
814
+ this.selectKeyboardNode(currentNode)
815
+ }
816
+
817
+ focusRelativeNode(currentNode, offset) {
818
+ const visibleNodes = this.visibleNodes()
819
+ const currentIndex = visibleNodes.indexOf(currentNode)
820
+ if (currentIndex === -1) return
821
+
822
+ const nextNode = visibleNodes[currentIndex + offset]
823
+ if (nextNode) this.setFocusedNode(nextNode)
824
+ }
825
+
826
+ focusFirstVisibleNode() {
827
+ const firstNode = this.visibleNodes()[0]
828
+ if (firstNode) this.setFocusedNode(firstNode)
829
+ }
830
+
831
+ focusLastVisibleNode() {
832
+ const visibleNodes = this.visibleNodes()
833
+ const lastNode = visibleNodes[visibleNodes.length - 1]
834
+ if (lastNode) this.setFocusedNode(lastNode)
835
+ }
836
+
837
+ focusTypeaheadMatch(currentNode, key) {
838
+ this.typeaheadBuffer += key.toLowerCase()
839
+ this.scheduleTypeaheadReset()
840
+
841
+ const visibleNodes = this.visibleNodes()
842
+ if (visibleNodes.length === 0) return
843
+
844
+ const currentIndex = visibleNodes.indexOf(currentNode)
845
+ const startIndex = currentIndex >= 0 ? currentIndex + 1 : 0
846
+ const candidates = visibleNodes.slice(startIndex).concat(visibleNodes.slice(0, startIndex))
847
+ const match = candidates.find((node) => this.nodeTitle(node).toLowerCase().startsWith(this.typeaheadBuffer))
848
+
849
+ if (match) this.setFocusedNode(match)
850
+ }
851
+
852
+ isTypeaheadKey(event) {
853
+ return event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey
854
+ }
855
+
856
+ scheduleTypeaheadReset() {
857
+ if (this.typeaheadTimeout) clearTimeout(this.typeaheadTimeout)
858
+
859
+ this.typeaheadTimeout = setTimeout(() => {
860
+ this.typeaheadBuffer = ""
861
+ this.typeaheadTimeout = null
862
+ }, 500)
863
+ }
864
+
865
+ clearTypeahead() {
866
+ if (this.typeaheadTimeout) {
867
+ clearTimeout(this.typeaheadTimeout)
868
+ this.typeaheadTimeout = null
869
+ }
870
+ this.typeaheadBuffer = ""
871
+ }
872
+
873
+ nodeTitle(node) {
874
+ return node.dataset.title || node.querySelector(".hakumi-tree-title-text")?.textContent || ""
875
+ }
876
+
877
+ expandOrFocusChild(node) {
878
+ if (node.dataset.leaf === "true") return
879
+
880
+ const key = node.dataset.key
881
+ if (!this.expandedKeys.has(key)) {
882
+ this.toggleNodeByKey(key)
883
+ this.setFocusedNode(node)
884
+ return
885
+ }
886
+
887
+ const firstChild = this.firstVisibleChild(node)
888
+ if (firstChild) this.setFocusedNode(firstChild)
889
+ }
890
+
891
+ collapseOrFocusParent(node) {
892
+ if (node.dataset.leaf !== "true" && this.expandedKeys.has(node.dataset.key)) {
893
+ this.toggleNodeByKey(node.dataset.key)
894
+ this.setFocusedNode(node)
895
+ return
896
+ }
897
+
898
+ const parent = this.parentNode(node)
899
+ if (parent) this.setFocusedNode(parent)
900
+ }
901
+
902
+ selectKeyboardNode(node) {
903
+ if (!this.canSelectNode(node)) return
904
+
905
+ this.selectNodeByKey(node.dataset.key)
906
+ }
907
+
908
+ canSelectNode(node) {
909
+ return (
910
+ this.selectableValue &&
911
+ node.dataset.selectable !== "false" &&
912
+ node.dataset.disabled !== "true" &&
913
+ !this.disabledValue
914
+ )
915
+ }
916
+
917
+ visibleNodes() {
918
+ return this.navigationNodeTargets().filter((node) => this.isVisibleNode(node))
919
+ }
920
+
921
+ navigationNodeTargets() {
922
+ if (this.hasHeightValue) return this.virtualHeightNodeTargets()
923
+
924
+ return this.nodeTargets
925
+ }
926
+
927
+ virtualHeightNodeTargets() {
928
+ return this.nodeTargets
929
+ }
930
+
931
+ isVisibleNode(node) {
932
+ if (node.classList.contains("hakumi-tree-node-hidden")) return false
933
+
934
+ let parent = this.parentNode(node)
935
+ while (parent) {
936
+ if (parent.classList.contains("hakumi-tree-node-hidden")) return false
937
+ if (!this.expandedKeys.has(parent.dataset.key)) return false
938
+ parent = this.parentNode(parent)
939
+ }
940
+
941
+ return true
942
+ }
943
+
944
+ firstVisibleChild(node) {
945
+ const entry = this.nodeMap.get(node.dataset.key)
946
+ if (!entry) return null
947
+
948
+ const childKey = entry.children.find((key) => {
949
+ const child = this.nodeMap.get(key)
950
+ return child && this.isVisibleNode(child.element)
951
+ })
952
+
953
+ return childKey ? this.nodeMap.get(childKey).element : null
954
+ }
955
+
956
+ parentNode(node) {
957
+ const parentKey = node.dataset.parentKey || null
958
+ if (!parentKey || !this.nodeMap.has(parentKey)) return null
959
+
960
+ return this.nodeMap.get(parentKey).element
961
+ }
962
+
963
+ syncRovingTabindex() {
964
+ const visibleNodes = this.visibleNodes()
965
+ const current = visibleNodes.find((node) => node.getAttribute("tabindex") === "0")
966
+ const selected = visibleNodes.find((node) => this.selectedKeys.has(node.dataset.key))
967
+ const focused = current || selected || visibleNodes[0] || null
968
+
969
+ this.nodeTargets.forEach((node) => {
970
+ node.setAttribute("tabindex", node === focused ? "0" : "-1")
971
+ this.syncNodeControlTabindex(node)
972
+ })
973
+
974
+ return focused
975
+ }
976
+
977
+ setFocusedNode(node, { focus = true } = {}) {
978
+ this.nodeTargets.forEach((target) => {
979
+ target.setAttribute("tabindex", target === node ? "0" : "-1")
980
+ this.syncNodeControlTabindex(target)
981
+ })
982
+
983
+ if (focus) node.focus()
984
+ }
985
+
986
+ syncNodeControlTabindex(node) {
987
+ node.querySelectorAll(".hakumi-tree-switcher, .hakumi-tree-checkbox, .hakumi-tree-title").forEach((control) => {
988
+ control.setAttribute("tabindex", "-1")
989
+ })
990
+ }
706
991
  }
@@ -30,11 +30,11 @@ export default class extends RegistryController {
30
30
 
31
31
  async copy(event) {
32
32
 
33
- const copyIcon = event.target?.closest('.hakumi-typography-copy')
33
+ const copyIcon = event?.target?.closest?.('.hakumi-typography-copy') || this.element.querySelector('.hakumi-typography-copy')
34
34
  if (!copyIcon) return
35
35
 
36
- event.preventDefault()
37
- event.stopPropagation()
36
+ event?.preventDefault?.()
37
+ event?.stopPropagation?.()
38
38
 
39
39
 
40
40
  const clone = this.element.cloneNode(true)