@hakumi-dev/hakumi-components 0.1.18-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 +208 -381
  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
@@ -1,5 +1,6 @@
1
1
  import RegistryController from "../base/registry_controller.js"
2
2
  import Sortable from "sortablejs"
3
+ import TableEditable from "./table/editable.js"
3
4
 
4
5
  export default class extends RegistryController {
5
6
 
@@ -45,7 +46,7 @@ export default class extends RegistryController {
45
46
  this._filters = new Map()
46
47
  this._sorter = null
47
48
  this._expandedRowKeys = new Set(this.#expandedRowKeysFromDom())
48
- this._shouldCellUpdateHooks = new Map()
49
+ this._editable = new TableEditable(this)
49
50
  this._rowSortable = null
50
51
  this._columnSortable = null
51
52
  this._pagination = { current: 1, pageSize: this.pageSizeValue || 10, total: 0 }
@@ -98,16 +99,7 @@ export default class extends RegistryController {
98
99
 
99
100
  disconnect() {
100
101
  this.#teardownScrollSync()
101
-
102
- // Clean up sortable instances
103
- if (this._rowSortable) {
104
- this._rowSortable.destroy()
105
- this._rowSortable = null
106
- }
107
- if (this._columnSortable) {
108
- this._columnSortable.destroy()
109
- this._columnSortable = null
110
- }
102
+ this.#teardownDragSorting()
111
103
 
112
104
  super.disconnect()
113
105
  }
@@ -325,15 +317,11 @@ export default class extends RegistryController {
325
317
  }
326
318
 
327
319
  registerShouldCellUpdate(name, fn) {
328
- if (typeof fn !== "function") {
329
- console.warn(`[Hakumi Table] registerShouldCellUpdate: "${name}" must be a function`)
330
- return
331
- }
332
- this._shouldCellUpdateHooks.set(name, fn)
320
+ this._editable.registerShouldCellUpdate(name, fn)
333
321
  }
334
322
 
335
323
  unregisterShouldCellUpdate(name) {
336
- this._shouldCellUpdateHooks.delete(name)
324
+ this._editable.unregisterShouldCellUpdate(name)
337
325
  }
338
326
 
339
327
  getRowOrder() {
@@ -350,44 +338,12 @@ export default class extends RegistryController {
350
338
 
351
339
  async loadData(options = {}) {
352
340
  if (!this.hasDataUrlValue) return null
353
-
354
- const page = options.page ?? this._pagination.current
355
- const pageSize = options.pageSize ?? this._pagination.pageSize
356
- const sorter = options.sorter ?? this._sorter
357
-
358
- const params = new URLSearchParams()
359
- params.set("page", page)
360
- params.set("per_page", pageSize)
361
- if (sorter?.field) {
362
- params.set("sort_field", sorter.field)
363
- params.set("sort_order", sorter.order || "ascend")
364
- }
365
-
366
- const fullUrl = `${this.dataUrlValue}?${params.toString()}`
367
-
341
+
368
342
  this.element.classList.add("hakumi-table-loading")
369
-
343
+
370
344
  try {
371
- const response = await fetch(fullUrl, {
372
- headers: { "Accept": "application/json" }
373
- })
374
-
375
- if (!response.ok) throw new Error(`HTTP ${response.status}`)
376
-
377
- const json = await response.json()
378
-
379
- this._pagination = {
380
- current: json.pagination.current,
381
- pageSize: json.pagination.pageSize,
382
- total: json.pagination.total
383
- }
384
-
385
- if (json.sorter) {
386
- this._sorter = json.sorter
387
- }
388
-
389
- this.#renderRows(json.data)
390
- this.#updatePagination()
345
+ const json = await this.#fetchData(this.#buildDataUrl(options))
346
+ this.#applyDataResponse(json)
391
347
  this.#dispatchLoadComplete(json)
392
348
  return json
393
349
  } catch (error) {
@@ -418,6 +374,52 @@ export default class extends RegistryController {
418
374
  return this.loadData()
419
375
  }
420
376
 
377
+ #buildDataUrl(options = {}) {
378
+ const { page, pageSize, sorter } = this.#dataRequestState(options)
379
+ const params = new URLSearchParams()
380
+ params.set("page", page)
381
+ params.set("per_page", pageSize)
382
+ if (sorter?.field) {
383
+ params.set("sort_field", sorter.field)
384
+ params.set("sort_order", sorter.order || "ascend")
385
+ }
386
+
387
+ return `${this.dataUrlValue}?${params.toString()}`
388
+ }
389
+
390
+ #dataRequestState(options = {}) {
391
+ return {
392
+ page: options.page ?? this._pagination.current,
393
+ pageSize: options.pageSize ?? this._pagination.pageSize,
394
+ sorter: options.sorter ?? this._sorter
395
+ }
396
+ }
397
+
398
+ async #fetchData(url) {
399
+ const response = await fetch(url, {
400
+ headers: { "Accept": "application/json" }
401
+ })
402
+
403
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
404
+
405
+ return response.json()
406
+ }
407
+
408
+ #applyDataResponse(json) {
409
+ this._pagination = {
410
+ current: json.pagination.current,
411
+ pageSize: json.pagination.pageSize,
412
+ total: json.pagination.total
413
+ }
414
+
415
+ if (json.sorter) {
416
+ this._sorter = json.sorter
417
+ }
418
+
419
+ this.#renderRows(json.data)
420
+ this.#updatePagination()
421
+ }
422
+
421
423
  #renderRows(data) {
422
424
  const tbody = this.element.querySelector("tbody")
423
425
  if (!tbody) return
@@ -469,29 +471,26 @@ export default class extends RegistryController {
469
471
  if (!this.hasDataUrlValue) return
470
472
  this.#syncPaginationFromComponent()
471
473
  this.#bindPaginationEvents()
472
- this.loadData()
474
+ this.loadData().catch(() => {})
473
475
  }
474
476
 
475
477
  #syncPaginationFromComponent() {
476
- const paginationEl = this.element.querySelector(".hakumi-table-pagination .hakumi-pagination")
478
+ const paginationEl = this.#paginationElement()
477
479
  if (!paginationEl) return
478
480
 
479
- const controller = this.application.getControllerForElementAndIdentifier(paginationEl, "hakumi--pagination")
481
+ const controller = this.#paginationController()
480
482
  if (controller) {
481
483
  this._pagination.pageSize = controller.pageSizeValue || this._pagination.pageSize
482
484
  }
483
485
  }
484
486
 
485
487
  #getPaginationComponent() {
486
- const paginationEl = this.element.querySelector(".hakumi-table-pagination .hakumi-pagination")
488
+ const paginationEl = this.#paginationElement()
487
489
  return paginationEl?.hakumiPagination || paginationEl?.hakumiComponent?.api
488
490
  }
489
491
 
490
492
  #updatePagination() {
491
- const paginationEl = this.element.querySelector(".hakumi-table-pagination .hakumi-pagination")
492
- if (!paginationEl) return
493
-
494
- const controller = this.application.getControllerForElementAndIdentifier(paginationEl, "hakumi--pagination")
493
+ const controller = this.#paginationController()
495
494
  if (!controller) return
496
495
 
497
496
  const { current, pageSize, total } = this._pagination
@@ -503,260 +502,51 @@ export default class extends RegistryController {
503
502
  }
504
503
 
505
504
  #bindPaginationEvents() {
506
- const container = this.element.querySelector(".hakumi-table-pagination")
505
+ const container = this.#paginationContainer()
507
506
  if (!container || container._paginationBound) return
508
507
  container._paginationBound = true
509
508
 
510
- container.addEventListener("hakumi--pagination:change", (e) => {
511
- e.stopPropagation()
512
- const page = e.detail.current
513
- if (page !== this._pagination.current) {
514
- this._pagination.current = page
515
- this.loadData()
516
- }
517
- })
509
+ container.addEventListener("hakumi--pagination:change", (e) => this.#handlePaginationChange(e))
518
510
 
519
- container.addEventListener("hakumi--pagination:sizeChange", (e) => {
520
- e.stopPropagation()
521
- const size = e.detail.pageSize
522
- if (size !== this._pagination.pageSize) {
523
- this._pagination.pageSize = size
524
- this._pagination.current = 1
525
- this.loadData()
526
- }
527
- })
511
+ container.addEventListener("hakumi--pagination:sizeChange", (e) => this.#handlePaginationSizeChange(e))
528
512
  }
529
513
 
530
- editCell(event) {
531
- const cell = event.currentTarget
532
- if (cell.querySelector(".hakumi-table-cell-editor")) return
533
-
534
- const tableConfig = this.editableConfigValue || {}
535
- const mode = tableConfig.mode || "cell"
536
-
537
- if (mode === "row") {
538
- this.#editRow(cell)
539
- } else {
540
- this.#editSingleCell(cell)
541
- }
514
+ #paginationContainer() {
515
+ return this.element.querySelector(".hakumi-table-pagination")
542
516
  }
543
517
 
544
- #editSingleCell(cell) {
545
- const { input, editor, originalContent, originalValue, config } = this.#createCellEditor(cell)
546
- input.focus()
547
-
548
- const rowElement = cell.closest("tr")
549
- const rowKey = rowElement?.dataset.rowKey
550
- const columnKey = config.key || cell.dataset.columnKey
551
- const dataIndex = config.data_index
552
-
553
- const save = () => {
554
- if (!editor.isConnected) return
555
-
556
- const newValue = input.value
557
- const detail = { rowKey, columnKey, dataIndex, value: newValue, originalValue }
558
-
559
- if (!this.#shouldUpdateCell(cell, detail)) {
560
- if (originalContent) originalContent.style.display = ""
561
- editor.remove()
562
- return
563
- }
564
-
565
- if (originalContent) {
566
- originalContent.textContent = newValue
567
- originalContent.style.display = ""
568
- }
569
- cell.dataset.value = newValue
570
- editor.remove()
571
-
572
- this.#dispatchEdit(detail)
573
- }
574
-
575
- const cancel = () => {
576
- if (!editor.isConnected) return
577
- if (originalContent) originalContent.style.display = ""
578
- editor.remove()
579
- }
580
-
581
- input.addEventListener("blur", save)
582
-
583
- input.addEventListener("keydown", (e) => {
584
- if (e.key === "Enter") {
585
- e.preventDefault()
586
- input.blur()
587
- } else if (e.key === "Escape") {
588
- e.preventDefault()
589
- input.removeEventListener("blur", save)
590
- cancel()
591
- }
592
- })
518
+ #paginationElement() {
519
+ return this.element.querySelector(".hakumi-table-pagination .hakumi-pagination")
593
520
  }
594
521
 
595
- #editRow(clickedCell) {
596
- const row = clickedCell.closest("tr")
597
- if (!row || row.classList.contains("hakumi-table-row-editing")) return
598
-
599
- row.classList.add("hakumi-table-row-editing")
600
- const rowKey = row.dataset.rowKey
601
- const editableCells = row.querySelectorAll("td[data-editable='true']")
602
- const editors = []
603
-
604
- editableCells.forEach((cell) => {
605
- const { input, originalContent, originalValue, config } = this.#createCellEditor(cell)
606
- input.dataset.columnKey = config.key || cell.dataset.columnKey
607
- input.dataset.dataIndex = config.data_index
608
- input.dataset.originalValue = originalValue
609
-
610
- editors.push({ cell, input, originalContent, originalValue, config })
611
- })
612
-
613
- const clickedInput = clickedCell.querySelector(".hakumi-table-cell-input")
614
- if (clickedInput) {
615
- clickedInput.focus()
616
- } else if (editors.length > 0) {
617
- editors[0].input.focus()
618
- }
619
-
620
- const saveRow = () => {
621
- const changes = []
622
- let allValid = true
522
+ #paginationController() {
523
+ const paginationEl = this.#paginationElement()
524
+ if (!paginationEl) return null
623
525
 
624
- editors.forEach(({ cell, input, originalContent, originalValue, config }) => {
625
- const newValue = input.value
626
- const columnKey = config.key || cell.dataset.columnKey
627
- const dataIndex = config.data_index
628
- const detail = { rowKey, columnKey, dataIndex, value: newValue, originalValue }
629
-
630
- if (!this.#shouldUpdateCell(cell, detail)) {
631
- allValid = false
632
- return
633
- }
634
-
635
- changes.push({ cell, originalContent, newValue, detail })
636
- })
637
-
638
- if (!allValid) {
639
- cancelRow()
640
- return
641
- }
642
-
643
- changes.forEach(({ cell, originalContent, newValue }) => {
644
- if (originalContent) {
645
- originalContent.textContent = newValue
646
- originalContent.style.display = ""
647
- }
648
- cell.dataset.value = newValue
649
- cell.querySelector(".hakumi-table-cell-editor")?.remove()
650
- })
651
-
652
- row.classList.remove("hakumi-table-row-editing")
653
-
654
- this.#dispatchRowEdit({
655
- rowKey,
656
- changes: changes.map(({ detail }) => detail)
657
- })
658
- }
659
-
660
- const cancelRow = () => {
661
- editors.forEach(({ cell, originalContent }) => {
662
- if (originalContent) originalContent.style.display = ""
663
- cell.querySelector(".hakumi-table-cell-editor")?.remove()
664
- })
665
- row.classList.remove("hakumi-table-row-editing")
666
- }
667
-
668
- editors.forEach(({ input }) => {
669
- input.addEventListener("keydown", (e) => {
670
- if (e.key === "Enter") {
671
- e.preventDefault()
672
- saveRow()
673
- } else if (e.key === "Escape") {
674
- e.preventDefault()
675
- cancelRow()
676
- }
677
- })
678
- })
679
-
680
- this.dispatch("rowEditing", {
681
- detail: { rowKey, save: saveRow, cancel: cancelRow }
682
- })
683
- }
684
-
685
- #dispatchEdit(detail) {
686
- this.dispatch("edit", { detail })
526
+ return this.application.getControllerForElementAndIdentifier(paginationEl, "hakumi--pagination")
687
527
  }
688
528
 
689
- #dispatchRowEdit(detail) {
690
- const params = this.#changesToParams(detail.changes)
691
-
692
- this.dispatch("rowEdit", {
693
- detail: {
694
- ...detail,
695
- params
696
- }
697
- })
698
- }
699
-
700
- #changesToParams(changes) {
701
- const params = {}
702
- if (!changes || !Array.isArray(changes)) return params
703
-
704
- changes.forEach(({ columnKey, dataIndex, value }) => {
705
- const key = dataIndex || columnKey
706
- if (key) {
707
- params[key] = value
708
- }
709
- })
710
- return params
711
- }
712
-
713
- #shouldUpdateCell(cell, detail) {
714
- const hookName = cell?.dataset.shouldCellUpdate
715
- if (!hookName) return true
716
-
717
- const hook = this.#resolveHook(hookName)
718
- if (typeof hook !== "function") {
719
- console.warn(`[Hakumi Table] shouldCellUpdate hook "${hookName}" is not a function`)
720
- return true
721
- }
722
-
723
- try {
724
- const result = hook(detail)
725
- if (result === false) return false
726
-
727
- if (typeof result === "string") {
728
- this.dispatch("cellUpdatePrevented", {
729
- detail: {
730
- ...detail,
731
- reason: result
732
- }
733
- })
734
- return false
735
- }
736
-
737
- return true
738
- } catch (error) {
739
- console.error(`[Hakumi Table] shouldCellUpdate hook "${hookName}" failed`, error)
740
- return true
529
+ #handlePaginationChange(event) {
530
+ event.stopPropagation()
531
+ const page = event.detail.current
532
+ if (page !== this._pagination.current) {
533
+ this._pagination.current = page
534
+ this.loadData()
741
535
  }
742
536
  }
743
537
 
744
- #resolveHook(name) {
745
- if (!name) return null
746
-
747
-
748
- if (this._shouldCellUpdateHooks.has(name)) {
749
- return this._shouldCellUpdateHooks.get(name)
750
- }
751
-
752
-
753
- if (typeof window === "undefined") return null
754
-
755
- if (window.HakumiTableShouldCellUpdate && typeof window.HakumiTableShouldCellUpdate[name] === "function") {
756
- return window.HakumiTableShouldCellUpdate[name]
538
+ #handlePaginationSizeChange(event) {
539
+ event.stopPropagation()
540
+ const size = event.detail.pageSize
541
+ if (size !== this._pagination.pageSize) {
542
+ this._pagination.pageSize = size
543
+ this._pagination.current = 1
544
+ this.loadData()
757
545
  }
546
+ }
758
547
 
759
- return name.split(".").reduce((acc, key) => (acc ? acc[key] : undefined), window)
548
+ editCell(event) {
549
+ this._editable.editCell(event)
760
550
  }
761
551
 
762
552
  #exposeApi(api) {
@@ -780,8 +570,9 @@ export default class extends RegistryController {
780
570
  })
781
571
 
782
572
 
783
- if (this._sorter && this._sorter.key && this._sorter.order) {
784
- url.searchParams.set(this.sortFieldParamValue, this._sorter.key)
573
+ const sorterKey = this._sorter?.field || this._sorter?.key
574
+ if (sorterKey && this._sorter.order) {
575
+ url.searchParams.set(this.sortFieldParamValue, sorterKey)
785
576
  url.searchParams.set(this.sortOrderParamValue, this._sorter.order)
786
577
  } else {
787
578
  url.searchParams.delete(this.sortFieldParamValue)
@@ -808,27 +599,6 @@ export default class extends RegistryController {
808
599
  }
809
600
  }
810
601
 
811
- #createCellEditor(cell) {
812
- const config = this.#parseJson(cell.dataset.editableConfig, {})
813
- const originalContent = cell.querySelector(".hakumi-table-cell-content")
814
- const originalValue = cell.dataset.value ?? (originalContent?.textContent?.trim() || "")
815
-
816
- const editor = document.createElement("div")
817
- editor.className = "hakumi-table-cell-editor"
818
-
819
- const input = document.createElement("input")
820
- input.type = config.input_type || "text"
821
- input.value = originalValue
822
- input.className = "hakumi-table-cell-input"
823
-
824
- editor.appendChild(input)
825
-
826
- if (originalContent) originalContent.style.display = "none"
827
- cell.appendChild(editor)
828
-
829
- return { input, editor, originalContent, originalValue, config }
830
- }
831
-
832
602
  #syncSelection() {
833
603
  const keys = this._selectedRowKeys
834
604
  this.selectionCheckboxTargets.forEach((input) => {
@@ -853,7 +623,7 @@ export default class extends RegistryController {
853
623
  }
854
624
 
855
625
  #expandedRowKeysFromDom() {
856
- const expandedRows = this.element.querySelectorAll("tr[data-row-type='expanded'].is-expanded")
626
+ const expandedRows = this.element.querySelectorAll("tr[data-row-type='expanded'].hakumi-table-row-expanded")
857
627
  return Array.from(expandedRows).map((row) => String(row.dataset.rowKey))
858
628
  }
859
629
 
@@ -967,13 +737,13 @@ export default class extends RegistryController {
967
737
  if (!expandedRow || expandedRow.dataset.rowType !== "expanded") return
968
738
  const expanded = this._expandedRowKeys.has(key)
969
739
 
970
- expandedRow.hidden = !expanded
971
- expandedRow.classList.toggle("is-expanded", expanded)
740
+ expandedRow.hidden = row.hidden || !expanded
741
+ expandedRow.classList.toggle("hakumi-table-row-expanded", expanded)
972
742
 
973
743
  const button = row.querySelector("[data-hakumi--table-target='expandToggle']")
974
744
  if (button) {
975
745
  button.setAttribute("aria-expanded", expanded)
976
- button.classList.toggle("is-expanded", expanded)
746
+ button.classList.toggle("hakumi-table-expand-button-expanded", expanded)
977
747
  }
978
748
  })
979
749
  }
@@ -1007,19 +777,8 @@ export default class extends RegistryController {
1007
777
  #syncScrollState() {
1008
778
  if (!this.hasScrollContainerTarget) return
1009
779
 
1010
- const el = this.scrollContainerTarget
1011
- const maxScrollLeft = el.scrollWidth - el.clientWidth
1012
- const hasLeft = el.scrollLeft > 0
1013
- const hasRight = el.scrollLeft < maxScrollLeft - 1
1014
-
1015
- this.element.classList.toggle("hakumi-table-scroll-left", hasLeft)
1016
- this.element.classList.toggle("hakumi-table-scroll-right", hasRight)
1017
-
1018
- if (this.hasScrollBarTarget && !this._syncingFromBar) {
1019
- this._syncingFromContainer = true
1020
- this.scrollBarTarget.scrollLeft = el.scrollLeft
1021
- this._syncingFromContainer = false
1022
- }
780
+ this.#syncScrollEdgeClasses()
781
+ this.#syncMirrorScrollBarFromContainer()
1023
782
  }
1024
783
 
1025
784
  #syncFromScrollBar() {
@@ -1027,6 +786,7 @@ export default class extends RegistryController {
1027
786
 
1028
787
  this._syncingFromBar = true
1029
788
  this.scrollContainerTarget.scrollLeft = this.scrollBarTarget.scrollLeft
789
+ this.#syncScrollState()
1030
790
  this._syncingFromBar = false
1031
791
  }
1032
792
 
@@ -1037,6 +797,24 @@ export default class extends RegistryController {
1037
797
  this.scrollBarThumbTarget.style.width = `${el.scrollWidth}px`
1038
798
  }
1039
799
 
800
+ #syncScrollEdgeClasses() {
801
+ const el = this.scrollContainerTarget
802
+ const maxScrollLeft = el.scrollWidth - el.clientWidth
803
+ const hasLeft = el.scrollLeft > 0
804
+ const hasRight = el.scrollLeft < maxScrollLeft - 1
805
+
806
+ this.element.classList.toggle("hakumi-table-scroll-left", hasLeft)
807
+ this.element.classList.toggle("hakumi-table-scroll-right", hasRight)
808
+ }
809
+
810
+ #syncMirrorScrollBarFromContainer() {
811
+ if (!this.hasScrollBarTarget || this._syncingFromBar) return
812
+
813
+ this._syncingFromContainer = true
814
+ this.scrollBarTarget.scrollLeft = this.scrollContainerTarget.scrollLeft
815
+ this._syncingFromContainer = false
816
+ }
817
+
1040
818
  #nextSortOrder(currentOrder) {
1041
819
  if (!currentOrder) return this._sortDirections[0]
1042
820
  const index = this._sortDirections.indexOf(currentOrder)
@@ -1077,62 +855,84 @@ export default class extends RegistryController {
1077
855
  }
1078
856
  }
1079
857
 
858
+ #teardownDragSorting() {
859
+ if (this._rowSortable) {
860
+ this._rowSortable.destroy()
861
+ this._rowSortable = null
862
+ }
863
+ if (this._columnSortable) {
864
+ this._columnSortable.destroy()
865
+ this._columnSortable = null
866
+ }
867
+ }
868
+
1080
869
  #setupRowDrag() {
1081
870
  const tbody = this.element.querySelector("tbody")
1082
871
  if (!tbody) return
1083
872
 
873
+ this._rowSortable = Sortable.create(tbody, this.#rowDragOptions())
874
+ }
875
+
876
+ #rowDragOptions() {
1084
877
  const options = {
1085
878
  animation: 150,
1086
879
  ghostClass: "hakumi-table-row-ghost",
1087
880
  chosenClass: "hakumi-table-row-chosen",
1088
881
  dragClass: "hakumi-table-row-drag",
1089
882
  filter: ".hakumi-table-expanded-row",
1090
- onEnd: (evt) => {
1091
- if (evt.oldIndex === evt.newIndex) return
1092
-
1093
- const row = evt.item
1094
- const rowKey = row.dataset.rowKey
1095
-
1096
-
1097
- const expandedRow = row.nextElementSibling
1098
- if (expandedRow?.dataset.rowType === "expanded") {
1099
- row.parentNode.insertBefore(expandedRow, row.nextSibling)
1100
- }
1101
-
1102
- this.#dispatchRowReorder(evt.oldIndex, evt.newIndex, rowKey)
1103
- }
883
+ onEnd: (evt) => this.#handleRowDragEnd(evt)
1104
884
  }
1105
885
 
1106
-
1107
886
  if (this.rowDragHandleValue) {
1108
887
  options.handle = ".hakumi-table-drag-handle"
1109
888
  }
1110
889
 
1111
- this._rowSortable = Sortable.create(tbody, options)
890
+ return options
891
+ }
892
+
893
+ #handleRowDragEnd(evt) {
894
+ if (evt.oldIndex === evt.newIndex) return
895
+
896
+ const row = evt.item
897
+ const rowKey = row.dataset.rowKey
898
+
899
+ this.#moveExpandedRowAfterDataRow(row)
900
+ this.#dispatchRowReorder(evt.oldIndex, evt.newIndex, rowKey)
901
+ }
902
+
903
+ #moveExpandedRowAfterDataRow(row) {
904
+ const expandedRow = row.nextElementSibling
905
+ if (expandedRow?.dataset.rowType === "expanded") {
906
+ row.parentNode.insertBefore(expandedRow, row.nextSibling)
907
+ }
1112
908
  }
1113
909
 
1114
910
  #setupColumnDrag() {
1115
911
  const headerRow = this.element.querySelector(".hakumi-table-header-row")
1116
912
  if (!headerRow) return
1117
913
 
1118
- this._columnSortable = Sortable.create(headerRow, {
914
+ this._columnSortable = Sortable.create(headerRow, this.#columnDragOptions())
915
+ }
916
+
917
+ #columnDragOptions() {
918
+ return {
1119
919
  animation: 150,
1120
920
  ghostClass: "hakumi-table-column-ghost",
1121
921
  chosenClass: "hakumi-table-column-chosen",
1122
922
  dragClass: "hakumi-table-column-drag",
1123
923
  filter: ".hakumi-table-selection-column, .hakumi-table-expand-column",
1124
- onEnd: (evt) => {
1125
- if (evt.oldIndex === evt.newIndex) return
1126
-
1127
- const th = evt.item
1128
- const columnKey = th.dataset.columnKey
1129
-
1130
-
1131
- this.#reorderBodyColumns(evt.oldIndex, evt.newIndex)
1132
-
1133
- this.#dispatchColumnReorder(evt.oldIndex, evt.newIndex, columnKey)
1134
- }
1135
- })
924
+ onEnd: (evt) => this.#handleColumnDragEnd(evt)
925
+ }
926
+ }
927
+
928
+ #handleColumnDragEnd(evt) {
929
+ if (evt.oldIndex === evt.newIndex) return
930
+
931
+ const th = evt.item
932
+ const columnKey = th.dataset.columnKey
933
+
934
+ this.#reorderBodyColumns(evt.oldIndex, evt.newIndex)
935
+ this.#dispatchColumnReorder(evt.oldIndex, evt.newIndex, columnKey)
1136
936
  }
1137
937
 
1138
938
  #reorderBodyColumns(oldIndex, newIndex) {