katalyst-tables 3.0.0.beta1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -2
  3. data/README.md +56 -190
  4. data/app/assets/builds/katalyst/tables.esm.js +17 -47
  5. data/app/assets/builds/katalyst/tables.js +17 -47
  6. data/app/assets/builds/katalyst/tables.min.js +1 -1
  7. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  8. data/app/components/concerns/katalyst/tables/has_table_content.rb +17 -8
  9. data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
  10. data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
  11. data/app/components/concerns/katalyst/tables/selectable.rb +18 -75
  12. data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
  13. data/app/components/katalyst/table_component.html.erb +4 -4
  14. data/app/components/katalyst/table_component.rb +271 -53
  15. data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
  16. data/app/components/katalyst/tables/body_row_component.rb +4 -31
  17. data/app/components/katalyst/tables/cell_component.rb +85 -0
  18. data/app/components/katalyst/tables/{body → cells}/boolean_component.rb +8 -2
  19. data/app/components/katalyst/tables/{body → cells}/currency_component.rb +7 -7
  20. data/app/components/katalyst/tables/{body → cells}/date_component.rb +12 -9
  21. data/app/components/katalyst/tables/{body → cells}/date_time_component.rb +13 -10
  22. data/app/components/katalyst/tables/{body → cells}/number_component.rb +5 -5
  23. data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
  24. data/app/components/katalyst/tables/{body → cells}/rich_text_component.rb +8 -5
  25. data/app/components/katalyst/tables/cells/select_component.rb +39 -0
  26. data/app/components/katalyst/tables/data.rb +30 -0
  27. data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
  28. data/app/components/katalyst/tables/header_row_component.rb +4 -25
  29. data/app/components/katalyst/tables/label.rb +37 -0
  30. data/app/components/katalyst/tables/orderable/form_component.rb +38 -0
  31. data/app/components/katalyst/tables/selectable/form_component.html.erb +3 -3
  32. data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
  33. data/app/controllers/concerns/katalyst/tables/backend.rb +2 -28
  34. data/app/helpers/katalyst/tables/frontend.rb +48 -2
  35. data/app/javascript/tables/application.js +0 -5
  36. data/app/javascript/tables/orderable/form_controller.js +8 -6
  37. data/app/javascript/tables/orderable/item_controller.js +9 -0
  38. data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
  39. data/app/models/concerns/katalyst/tables/collection/sorting.rb +85 -17
  40. data/app/models/katalyst/tables/collection/array.rb +38 -0
  41. data/app/models/katalyst/tables/collection/base.rb +4 -0
  42. data/config/locales/tables.en.yml +0 -6
  43. data/lib/katalyst/tables/config.rb +23 -0
  44. data/lib/katalyst/tables.rb +9 -0
  45. metadata +22 -29
  46. data/app/components/concerns/katalyst/tables/body/typed_columns.rb +0 -132
  47. data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
  48. data/app/components/concerns/katalyst/tables/header/typed_columns.rb +0 -179
  49. data/app/components/katalyst/tables/body/attachment_component.rb +0 -58
  50. data/app/components/katalyst/tables/body/link_component.rb +0 -40
  51. data/app/components/katalyst/tables/body_cell_component.rb +0 -55
  52. data/app/components/katalyst/tables/header/attachment_component.rb +0 -15
  53. data/app/components/katalyst/tables/header/boolean_component.rb +0 -15
  54. data/app/components/katalyst/tables/header/currency_component.rb +0 -15
  55. data/app/components/katalyst/tables/header/date_component.rb +0 -15
  56. data/app/components/katalyst/tables/header/date_time_component.rb +0 -15
  57. data/app/components/katalyst/tables/header/link_component.rb +0 -15
  58. data/app/components/katalyst/tables/header/number_component.rb +0 -15
  59. data/app/components/katalyst/tables/header/rich_text_component.rb +0 -15
  60. data/app/components/katalyst/tables/header_cell_component.rb +0 -97
  61. data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
  62. data/app/javascript/tables/turbo/collection_controller.js +0 -38
  63. data/app/models/katalyst/tables/collection/sort_form.rb +0 -120
@@ -1 +1 @@
1
- {"version":3,"file":"tables.min.js","sources":["../../../javascript/tables/orderable/list_controller.js","../../../javascript/tables/selection/item_controller.js","../../../javascript/tables/application.js","../../../javascript/tables/turbo/collection_controller.js","../../../javascript/tables/orderable/item_controller.js","../../../javascript/tables/orderable/form_controller.js","../../../javascript/tables/selection/form_controller.js"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableListController extends Controller {\n static outlets = [\"tables--orderable--item\", \"tables--orderable--form\"];\n\n //region State transitions\n\n startDragging(dragState) {\n this.dragState = dragState;\n\n document.addEventListener(\"mousemove\", this.mousemove);\n document.addEventListener(\"mouseup\", this.mouseup);\n window.addEventListener(\"scroll\", this.scroll, true);\n\n this.element.style.position = \"relative\";\n }\n\n stopDragging() {\n const dragState = this.dragState;\n delete this.dragState;\n\n document.removeEventListener(\"mousemove\", this.mousemove);\n document.removeEventListener(\"mouseup\", this.mouseup);\n window.removeEventListener(\"scroll\", this.scroll, true);\n\n this.element.removeAttribute(\"style\");\n this.tablesOrderableItemOutlets.forEach((item) => item.reset());\n\n return dragState;\n }\n\n drop() {\n // note: early returns guard against turbo updates that prevent us finding\n // the right item to drop on. In this situation it's better to discard the\n // drop than to drop in the wrong place.\n\n const dragItem = this.dragItem;\n\n if (!dragItem) return;\n\n const newIndex = dragItem.dragIndex;\n const targetItem = this.tablesOrderableItemOutlets[newIndex];\n\n if (!targetItem) return;\n\n // swap the dragged item into the correct position for its current offset\n if (newIndex < dragItem.index) {\n targetItem.row.insertAdjacentElement(\"beforebegin\", dragItem.row);\n } else if (newIndex > dragItem.index) {\n targetItem.row.insertAdjacentElement(\"afterend\", dragItem.row);\n }\n\n // reindex all items based on their new positions\n this.tablesOrderableItemOutlets.forEach((item, index) =>\n item.updateIndex(index),\n );\n\n // save the changes\n this.commitChanges();\n }\n\n commitChanges() {\n // clear any existing inputs to prevent duplicates\n this.tablesOrderableFormOutlet.clear();\n\n // insert any items that have changed position\n this.tablesOrderableItemOutlets.forEach((item) => {\n if (item.hasChanges) this.tablesOrderableFormOutlet.add(item);\n });\n\n this.tablesOrderableFormOutlet.submit();\n }\n\n //endregion\n\n //region Events\n\n mousedown(event) {\n if (this.isDragging) return;\n\n const target = this.#targetItem(event.target);\n\n if (!target) return;\n\n event.preventDefault(); // prevent built-in drag\n\n this.startDragging(new DragState(this.element, event, target.id));\n\n this.dragState.updateCursor(this.element, target.row, event, this.animate);\n }\n\n mousemove = (event) => {\n if (!this.isDragging) return;\n\n event.preventDefault(); // prevent build-in drag\n\n if (this.ticking) return;\n\n this.ticking = true;\n\n window.requestAnimationFrame(() => {\n this.ticking = false;\n this.dragState.updateCursor(\n this.element,\n this.dragItem.row,\n event,\n this.animate,\n );\n });\n };\n\n scroll = (event) => {\n if (!this.isDragging || this.ticking) return;\n\n this.ticking = true;\n\n window.requestAnimationFrame(() => {\n this.ticking = false;\n this.dragState.updateScroll(\n this.element,\n this.dragItem.row,\n this.animate,\n );\n });\n };\n\n mouseup = (event) => {\n if (!this.isDragging) return;\n\n this.drop();\n this.stopDragging();\n this.tablesOrderableFormOutlets.forEach((form) => delete form.dragState);\n };\n\n tablesOrderableFormOutletConnected(form, element) {\n if (form.dragState) {\n // restore the previous controller's state\n this.startDragging(form.dragState);\n }\n }\n\n tablesOrderableFormOutletDisconnected(form, element) {\n if (this.isDragging) {\n // cache drag state in the form\n form.dragState = this.stopDragging();\n }\n }\n\n //endregion\n\n //region Helpers\n\n /**\n * Updates the position of the drag item with a relative offset. Updates\n * other items relative to the new position of the drag item, as required.\n *\n * @callback {OrderableListController~animate}\n * @param {number} offset\n */\n animate = (offset) => {\n const dragItem = this.dragItem;\n\n // Visually update the dragItem so it follows the cursor\n dragItem.dragUpdate(offset);\n\n // Visually updates the position of all items in the list relative to the\n // dragged item. No actual changes to orderings at this stage.\n this.#currentItems.forEach((item, index) => {\n if (item === dragItem) return;\n item.updateVisually(index);\n });\n };\n\n get isDragging() {\n return !!this.dragState;\n }\n\n get dragItem() {\n if (!this.isDragging) return null;\n\n return this.tablesOrderableItemOutlets.find(\n (item) => item.id === this.dragState.targetId,\n );\n }\n\n /**\n * Returns the current items in the list, sorted by their current index.\n * Current uses the drag index if the item is being dragged, if set.\n *\n * @returns {Array[OrderableRowController]}\n */\n get #currentItems() {\n return this.tablesOrderableItemOutlets.toSorted(\n (a, b) => a.comparisonIndex - b.comparisonIndex,\n );\n }\n\n /**\n * Returns the item outlet that was clicked on, if any.\n *\n * @param element {HTMLElement} the clicked ordinal cell\n * @returns {OrderableRowController}\n */\n #targetItem(element) {\n return this.tablesOrderableItemOutlets.find(\n (item) => item.element === element,\n );\n }\n\n //endregion\n}\n\n/**\n * During drag we want to be able to translate a document-relative coordinate\n * into a coordinate relative to the list element. This state object calculates\n * and stores internal state so that we can translate absolute page coordinates\n * from mouse events into relative offsets for the list items within the list\n * element.\n *\n * We also keep track of the drag target so that if the controller is attached\n * to a new element during the drag we can continue after the turbo update.\n */\nclass DragState {\n /**\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param event {MouseEvent} the initial event\n * @param id {String} the id of the element being dragged\n */\n constructor(list, event, id) {\n // cursor offset is the offset of the cursor relative to the drag item\n this.cursorOffset = event.offsetY;\n\n // initial offset is the offset position of the drag item at drag start\n this.initialPosition = event.target.offsetTop - list.offsetTop;\n\n // id of the item being dragged\n this.targetId = id;\n }\n\n /**\n * Calculates the offset of the drag item relative to its initial position.\n *\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param row {HTMLElement} the row being dragged\n * @param event {MouseEvent} the current event\n * @param callback {OrderableListController~animate} updates the drag item with a relative offset\n */\n updateCursor(list, row, event, callback) {\n // Calculate and store the list offset relative to the viewport\n // This value is cached so we can calculate the outcome of any scroll events\n this.listOffset = list.getBoundingClientRect().top;\n\n // Calculate the position of the cursor relative to the list.\n // Accounts for scroll offsets by using the item's bounding client rect.\n const cursorPosition = event.clientY - this.listOffset;\n\n // intended item position relative to the list, from cursor position\n let itemPosition = cursorPosition - this.cursorOffset;\n\n this.#updateItemPosition(list, row, itemPosition, callback);\n }\n\n /**\n * Animates the item's position as the list scrolls. Requires a previous call\n * to set the scroll offset.\n *\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param row {HTMLElement} the row being dragged\n * @param callback {OrderableListController~animate} updates the drag item with a relative offset\n */\n updateScroll(list, row, callback) {\n const previousScrollOffset = this.listOffset;\n\n // Calculate and store the list offset relative to the viewport\n // This value is cached so we can calculate the outcome of any scroll events\n this.listOffset = list.getBoundingClientRect().top;\n\n // Calculate the change in scroll offset since the last update\n const scrollDelta = previousScrollOffset - this.listOffset;\n\n // intended item position relative to the list, from cursor position\n const position = this.position + scrollDelta;\n\n this.#updateItemPosition(list, row, position, callback);\n }\n\n #updateItemPosition(list, row, position, callback) {\n // ensure itemPosition is within the bounds of the list (tbody)\n position = Math.max(position, 0);\n position = Math.min(position, list.offsetHeight - row.offsetHeight);\n\n // cache the item's position relative to the list for use in scroll events\n this.position = position;\n\n // Item has position: relative, so we want to calculate the amount to move\n // the item relative to it's DOM position to represent how much it has been\n // dragged by.\n const offset = position - this.initialPosition;\n\n // Convert itemPosition from offset relative to list to offset relative to\n // its position within the DOM (if it hadn't moved).\n callback(offset);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class SelectionItemController extends Controller {\n static outlets = [\"tables--selection--form\"];\n static values = {\n params: Object,\n checked: Boolean,\n };\n\n tablesSelectionFormOutletConnected(form) {\n this.checkedValue = form.isSelected(this.id);\n }\n\n change(e) {\n e.preventDefault();\n\n this.checkedValue = this.tablesSelectionFormOutlet.toggle(this.id);\n }\n\n get id() {\n return this.paramsValue.id;\n }\n\n checkedValueChanged(checked) {\n this.element.querySelector(\"input\").checked = checked;\n }\n}\n","import TurboCollectionController from \"./turbo/collection_controller\";\nimport OrderableItemController from \"./orderable/item_controller\";\nimport OrderableListController from \"./orderable/list_controller\";\nimport OrderableFormController from \"./orderable/form_controller\";\nimport SelectionFormController from \"./selection/form_controller\";\nimport SelectionItemController from \"./selection/item_controller\";\n\nconst Definitions = [\n {\n identifier: \"tables--turbo--collection\",\n controllerConstructor: TurboCollectionController,\n },\n {\n identifier: \"tables--orderable--item\",\n controllerConstructor: OrderableItemController,\n },\n {\n identifier: \"tables--orderable--list\",\n controllerConstructor: OrderableListController,\n },\n {\n identifier: \"tables--orderable--form\",\n controllerConstructor: OrderableFormController,\n },\n {\n identifier: \"tables--selection--form\",\n controllerConstructor: SelectionFormController,\n },\n {\n identifier: \"tables--selection--item\",\n controllerConstructor: SelectionItemController,\n },\n];\n\nexport { Definitions as default };\n","import { Controller } from \"@hotwired/stimulus\";\nimport { Turbo } from \"@hotwired/turbo-rails\";\n\nexport default class TurboCollectionController extends Controller {\n static values = {\n query: String,\n sort: String,\n };\n\n queryValueChanged(query) {\n Turbo.navigator.history.replace(this.#url(query));\n }\n\n sortValueChanged(sort) {\n document.querySelectorAll(this.#sortSelector).forEach((input) => {\n if (input) input.value = sort;\n });\n }\n\n get #sortSelector() {\n return \"input[name='sort']\";\n }\n\n #url(query) {\n const frame = this.element.closest(\"turbo-frame\");\n let url;\n\n if (frame) {\n url = new URL(frame.baseURI);\n } else {\n url = new URL(window.location.href);\n }\n\n url.search = query;\n\n return url;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableRowController extends Controller {\n static values = {\n params: Object,\n };\n\n connect() {\n // index from server may be inconsistent with the visual ordering,\n // especially if this is a new node. Use positional indexes instead,\n // as these are the values we will send on save.\n this.index = domIndex(this.row);\n }\n\n paramsValueChanged(params) {\n this.id = params.id_value;\n }\n\n dragUpdate(offset) {\n this.dragOffset = offset;\n this.row.style.position = \"relative\";\n this.row.style.top = offset + \"px\";\n this.row.style.zIndex = \"1\";\n this.row.toggleAttribute(\"dragging\", true);\n }\n\n /**\n * Called on items that are not the dragged item during drag. Updates the\n * visual position of the item relative to the dragged item.\n *\n * @param index {number} intended index of the item during drag\n */\n updateVisually(index) {\n this.row.style.position = \"relative\";\n this.row.style.top = `${\n this.row.offsetHeight * (index - this.dragIndex)\n }px`;\n }\n\n /**\n * Set the index value of the item. This is called on all items after a drop\n * event. If the index is different to the params index then this item has\n * changed.\n *\n * @param index {number} the new index value\n */\n updateIndex(index) {\n this.index = index;\n }\n\n /**\n * Restore any visual changes made during drag and remove the drag state.\n */\n reset() {\n delete this.dragOffset;\n this.row.removeAttribute(\"style\");\n this.row.removeAttribute(\"dragging\");\n }\n\n /**\n * @returns {boolean} true when the item has a change to its index value\n */\n get hasChanges() {\n return this.paramsValue.index_value !== this.index;\n }\n\n /**\n * Calculate the relative index of the item during drag. This is used to\n * sort items during drag as it takes into account any uncommitted changes\n * to index caused by the drag offset.\n *\n * @returns {number} index for the purposes of drag and drop ordering\n */\n get dragIndex() {\n if (this.dragOffset && this.dragOffset !== 0) {\n return this.index + Math.round(this.dragOffset / this.row.offsetHeight);\n } else {\n return this.index;\n }\n }\n\n /**\n * Index value for use in comparisons during drag. This is used to determine\n * whether the dragged item is above or below another item. If this item is\n * being dragged then we offset the index by 0.5 to ensure that it jumps up\n * or down when it reaches the midpoint of the item above or below it.\n *\n * @returns {number}\n */\n get comparisonIndex() {\n if (this.dragOffset) {\n return this.dragIndex + (this.dragOffset > 0 ? 0.5 : -0.5);\n } else {\n return this.index;\n }\n }\n\n /**\n * The containing row element.\n *\n * @returns {HTMLElement}\n */\n get row() {\n return this.element.parentElement;\n }\n}\n\nfunction domIndex(element) {\n return Array.from(element.parentElement.children).indexOf(element);\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableFormController extends Controller {\n add(item) {\n const { id_name, id_value, index_name } = item.paramsValue;\n this.element.insertAdjacentHTML(\n \"beforeend\",\n `<input type=\"hidden\" name=\"${id_name}\" value=\"${id_value}\" data-generated>\n <input type=\"hidden\" name=\"${index_name}\" value=\"${item.index}\" data-generated>`,\n );\n }\n\n submit() {\n if (this.inputs.length === 0) return;\n\n this.element.requestSubmit();\n }\n\n clear() {\n this.inputs.forEach((input) => input.remove());\n }\n\n get inputs() {\n return this.element.querySelectorAll(\"input[data-generated]\");\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class SelectionFormController extends Controller {\n static values = {\n count: Number,\n primaryKey: { type: String, default: \"id\" },\n };\n static targets = [\"count\", \"singular\", \"plural\"];\n\n connect() {\n this.countValue = this.inputs.length;\n }\n\n /**\n * @param id to toggle\n * @return {boolean} true if selected, false if unselected\n */\n toggle(id) {\n const input = this.input(id);\n\n if (input) {\n input.remove();\n } else {\n this.element.insertAdjacentHTML(\n \"beforeend\",\n `<input type=\"hidden\" name=\"${this.primaryKeyValue}[]\" value=\"${id}\">`,\n );\n }\n\n this.countValue = this.inputs.length;\n\n return !input;\n }\n\n /**\n * @returns {boolean} true if the given id is currently selected\n */\n isSelected(id) {\n return !!this.input(id);\n }\n\n get inputs() {\n return this.element.querySelectorAll(\n `input[name=\"${this.primaryKeyValue}[]\"]`,\n );\n }\n\n input(id) {\n return this.element.querySelector(\n `input[name=\"${this.primaryKeyValue}[]\"][value=\"${id}\"]`,\n );\n }\n\n countValueChanged(count) {\n this.element.toggleAttribute(\"hidden\", count === 0);\n this.countTarget.textContent = count;\n this.singularTarget.toggleAttribute(\"hidden\", count !== 1);\n this.pluralTarget.toggleAttribute(\"hidden\", count === 1);\n }\n}\n"],"names":["DragState","constructor","list","event","id","this","cursorOffset","offsetY","initialPosition","target","offsetTop","targetId","updateCursor","row","callback","listOffset","getBoundingClientRect","top","itemPosition","clientY","updateItemPosition","updateScroll","previousScrollOffset","scrollDelta","position","Math","max","min","offsetHeight","SelectionItemController","Controller","static","params","Object","checked","Boolean","tablesSelectionFormOutletConnected","form","checkedValue","isSelected","change","e","preventDefault","tablesSelectionFormOutlet","toggle","paramsValue","checkedValueChanged","element","querySelector","Definitions","identifier","controllerConstructor","query","String","sort","queryValueChanged","Turbo","navigator","history","replace","url","sortValueChanged","document","querySelectorAll","sortSelector","forEach","input","value","frame","closest","URL","baseURI","window","location","href","search","connect","index","Array","from","parentElement","children","indexOf","paramsValueChanged","id_value","dragUpdate","offset","dragOffset","style","zIndex","toggleAttribute","updateVisually","dragIndex","updateIndex","reset","removeAttribute","hasChanges","index_value","round","comparisonIndex","startDragging","dragState","addEventListener","mousemove","mouseup","scroll","stopDragging","removeEventListener","tablesOrderableItemOutlets","item","drop","dragItem","newIndex","targetItem","insertAdjacentElement","commitChanges","tablesOrderableFormOutlet","clear","add","submit","mousedown","isDragging","animate","ticking","requestAnimationFrame","tablesOrderableFormOutlets","tablesOrderableFormOutletConnected","tablesOrderableFormOutletDisconnected","currentItems","find","toSorted","a","b","id_name","index_name","insertAdjacentHTML","inputs","length","requestSubmit","remove","count","Number","primaryKey","type","default","countValue","primaryKeyValue","countValueChanged","countTarget","textContent","singularTarget","pluralTarget"],"mappings":"8FA8NA,MAAMA,EAMJ,WAAAC,CAAYC,EAAMC,EAAOC,GAEvBC,KAAKC,aAAeH,EAAMI,QAG1BF,KAAKG,gBAAkBL,EAAMM,OAAOC,UAAYR,EAAKQ,UAGrDL,KAAKM,SAAWP,CACjB,CAUD,YAAAQ,CAAaV,EAAMW,EAAKV,EAAOW,GAG7BT,KAAKU,WAAab,EAAKc,wBAAwBC,IAO/C,IAAIC,EAHmBf,EAAMgB,QAAUd,KAAKU,WAGRV,KAAKC,aAEzCD,MAAKe,EAAoBlB,EAAMW,EAAKK,EAAcJ,EACnD,CAUD,YAAAO,CAAanB,EAAMW,EAAKC,GACtB,MAAMQ,EAAuBjB,KAAKU,WAIlCV,KAAKU,WAAab,EAAKc,wBAAwBC,IAG/C,MAAMM,EAAcD,EAAuBjB,KAAKU,WAG1CS,EAAWnB,KAAKmB,SAAWD,EAEjClB,MAAKe,EAAoBlB,EAAMW,EAAKW,EAAUV,EAC/C,CAED,EAAAM,CAAoBlB,EAAMW,EAAKW,EAAUV,GAEvCU,EAAWC,KAAKC,IAAIF,EAAU,GAC9BA,EAAWC,KAAKE,IAAIH,EAAUtB,EAAK0B,aAAef,EAAIe,cAGtDvB,KAAKmB,SAAWA,EAShBV,EAJeU,EAAWnB,KAAKG,gBAKhC,EC5SY,MAAMqB,UAAgCC,EACnDC,eAAiB,CAAC,2BAClBA,cAAgB,CACdC,OAAQC,OACRC,QAASC,SAGX,kCAAAC,CAAmCC,GACjChC,KAAKiC,aAAeD,EAAKE,WAAWlC,KAAKD,GAC1C,CAED,MAAAoC,CAAOC,GACLA,EAAEC,iBAEFrC,KAAKiC,aAAejC,KAAKsC,0BAA0BC,OAAOvC,KAAKD,GAChE,CAED,MAAIA,GACF,OAAOC,KAAKwC,YAAYzC,EACzB,CAED,mBAAA0C,CAAoBZ,GAClB7B,KAAK0C,QAAQC,cAAc,SAASd,QAAUA,CAC/C,EClBE,MAACe,EAAc,CAClB,CACEC,WAAY,4BACZC,sBCPW,cAAwCrB,EACrDC,cAAgB,CACdqB,MAAOC,OACPC,KAAMD,QAGR,iBAAAE,CAAkBH,GAChBI,EAAMC,UAAUC,QAAQC,QAAQtD,MAAKuD,EAAKR,GAC3C,CAED,gBAAAS,CAAiBP,GACfQ,SAASC,iBAAiB1D,MAAK2D,GAAeC,SAASC,IACjDA,IAAOA,EAAMC,MAAQb,EAAI,GAEhC,CAED,KAAIU,GACF,MAAO,oBACR,CAED,EAAAJ,CAAKR,GACH,MAAMgB,EAAQ/D,KAAK0C,QAAQsB,QAAQ,eACnC,IAAIT,EAUJ,OAPEA,EADEQ,EACI,IAAIE,IAAIF,EAAMG,SAEd,IAAID,IAAIE,OAAOC,SAASC,MAGhCd,EAAIe,OAASvB,EAENQ,CACR,IDxBD,CACEV,WAAY,0BACZC,sBEZW,cAAqCrB,EAClDC,cAAgB,CACdC,OAAQC,QAGV,OAAA2C,GAoGF,IAAkB7B,EAhGd1C,KAAKwE,OAgGS9B,EAhGQ1C,KAAKQ,IAiGtBiE,MAAMC,KAAKhC,EAAQiC,cAAcC,UAAUC,QAAQnC,GAhGzD,CAED,kBAAAoC,CAAmBnD,GACjB3B,KAAKD,GAAK4B,EAAOoD,QAClB,CAED,UAAAC,CAAWC,GACTjF,KAAKkF,WAAaD,EAClBjF,KAAKQ,IAAI2E,MAAMhE,SAAW,WAC1BnB,KAAKQ,IAAI2E,MAAMvE,IAAMqE,EAAS,KAC9BjF,KAAKQ,IAAI2E,MAAMC,OAAS,IACxBpF,KAAKQ,IAAI6E,gBAAgB,YAAY,EACtC,CAQD,cAAAC,CAAed,GACbxE,KAAKQ,IAAI2E,MAAMhE,SAAW,WAC1BnB,KAAKQ,IAAI2E,MAAMvE,IACbZ,KAAKQ,IAAIe,cAAgBiD,EAAQxE,KAAKuF,WADnB,IAGtB,CASD,WAAAC,CAAYhB,GACVxE,KAAKwE,MAAQA,CACd,CAKD,KAAAiB,UACSzF,KAAKkF,WACZlF,KAAKQ,IAAIkF,gBAAgB,SACzB1F,KAAKQ,IAAIkF,gBAAgB,WAC1B,CAKD,cAAIC,GACF,OAAO3F,KAAKwC,YAAYoD,cAAgB5F,KAAKwE,KAC9C,CASD,aAAIe,GACF,OAAIvF,KAAKkF,YAAkC,IAApBlF,KAAKkF,WACnBlF,KAAKwE,MAAQpD,KAAKyE,MAAM7F,KAAKkF,WAAalF,KAAKQ,IAAIe,cAEnDvB,KAAKwE,KAEf,CAUD,mBAAIsB,GACF,OAAI9F,KAAKkF,WACAlF,KAAKuF,WAAavF,KAAKkF,WAAa,EAAI,IAAO,IAE/ClF,KAAKwE,KAEf,CAOD,OAAIhE,GACF,OAAOR,KAAK0C,QAAQiC,aACrB,IFxFD,CACE9B,WAAY,0BACZC,sBFhBW,cAAsCrB,EACnDC,eAAiB,CAAC,0BAA2B,2BAI7C,aAAAqE,CAAcC,GACZhG,KAAKgG,UAAYA,EAEjBvC,SAASwC,iBAAiB,YAAajG,KAAKkG,WAC5CzC,SAASwC,iBAAiB,UAAWjG,KAAKmG,SAC1ChC,OAAO8B,iBAAiB,SAAUjG,KAAKoG,QAAQ,GAE/CpG,KAAK0C,QAAQyC,MAAMhE,SAAW,UAC/B,CAED,YAAAkF,GACE,MAAML,EAAYhG,KAAKgG,UAUvB,cATOhG,KAAKgG,UAEZvC,SAAS6C,oBAAoB,YAAatG,KAAKkG,WAC/CzC,SAAS6C,oBAAoB,UAAWtG,KAAKmG,SAC7ChC,OAAOmC,oBAAoB,SAAUtG,KAAKoG,QAAQ,GAElDpG,KAAK0C,QAAQgD,gBAAgB,SAC7B1F,KAAKuG,2BAA2B3C,SAAS4C,GAASA,EAAKf,UAEhDO,CACR,CAED,IAAAS,GAKE,MAAMC,EAAW1G,KAAK0G,SAEtB,IAAKA,EAAU,OAEf,MAAMC,EAAWD,EAASnB,UACpBqB,EAAa5G,KAAKuG,2BAA2BI,GAE9CC,IAGDD,EAAWD,EAASlC,MACtBoC,EAAWpG,IAAIqG,sBAAsB,cAAeH,EAASlG,KACpDmG,EAAWD,EAASlC,OAC7BoC,EAAWpG,IAAIqG,sBAAsB,WAAYH,EAASlG,KAI5DR,KAAKuG,2BAA2B3C,SAAQ,CAAC4C,EAAMhC,IAC7CgC,EAAKhB,YAAYhB,KAInBxE,KAAK8G,gBACN,CAED,aAAAA,GAEE9G,KAAK+G,0BAA0BC,QAG/BhH,KAAKuG,2BAA2B3C,SAAS4C,IACnCA,EAAKb,YAAY3F,KAAK+G,0BAA0BE,IAAIT,EAAK,IAG/DxG,KAAK+G,0BAA0BG,QAChC,CAMD,SAAAC,CAAUrH,GACR,GAAIE,KAAKoH,WAAY,OAErB,MAAMhH,EAASJ,MAAK4G,EAAY9G,EAAMM,QAEjCA,IAELN,EAAMuC,iBAENrC,KAAK+F,cAAc,IAAIpG,EAAUK,KAAK0C,QAAS5C,EAAOM,EAAOL,KAE7DC,KAAKgG,UAAUzF,aAAaP,KAAK0C,QAAStC,EAAOI,IAAKV,EAAOE,KAAKqH,SACnE,CAEDnB,UAAapG,IACNE,KAAKoH,aAEVtH,EAAMuC,iBAEFrC,KAAKsH,UAETtH,KAAKsH,SAAU,EAEfnD,OAAOoD,uBAAsB,KAC3BvH,KAAKsH,SAAU,EACftH,KAAKgG,UAAUzF,aACbP,KAAK0C,QACL1C,KAAK0G,SAASlG,IACdV,EACAE,KAAKqH,QACN,KACD,EAGJjB,OAAUtG,IACHE,KAAKoH,aAAcpH,KAAKsH,UAE7BtH,KAAKsH,SAAU,EAEfnD,OAAOoD,uBAAsB,KAC3BvH,KAAKsH,SAAU,EACftH,KAAKgG,UAAUhF,aACbhB,KAAK0C,QACL1C,KAAK0G,SAASlG,IACdR,KAAKqH,QACN,IACD,EAGJlB,QAAWrG,IACJE,KAAKoH,aAEVpH,KAAKyG,OACLzG,KAAKqG,eACLrG,KAAKwH,2BAA2B5D,SAAS5B,UAAgBA,EAAKgE,YAAU,EAG1E,kCAAAyB,CAAmCzF,EAAMU,GACnCV,EAAKgE,WAEPhG,KAAK+F,cAAc/D,EAAKgE,UAE3B,CAED,qCAAA0B,CAAsC1F,EAAMU,GACtC1C,KAAKoH,aAEPpF,EAAKgE,UAAYhG,KAAKqG,eAEzB,CAaDgB,QAAWpC,IACT,MAAMyB,EAAW1G,KAAK0G,SAGtBA,EAAS1B,WAAWC,GAIpBjF,MAAK2H,EAAc/D,SAAQ,CAAC4C,EAAMhC,KAC5BgC,IAASE,GACbF,EAAKlB,eAAed,EAAM,GAC1B,EAGJ,cAAI4C,GACF,QAASpH,KAAKgG,SACf,CAED,YAAIU,GACF,OAAK1G,KAAKoH,WAEHpH,KAAKuG,2BAA2BqB,MACpCpB,GAASA,EAAKzG,KAAOC,KAAKgG,UAAU1F,WAHV,IAK9B,CAQD,KAAIqH,GACF,OAAO3H,KAAKuG,2BAA2BsB,UACrC,CAACC,EAAGC,IAAMD,EAAEhC,gBAAkBiC,EAAEjC,iBAEnC,CAQD,EAAAc,CAAYlE,GACV,OAAO1C,KAAKuG,2BAA2BqB,MACpCpB,GAASA,EAAK9D,UAAYA,GAE9B,IE3LD,CACEG,WAAY,0BACZC,sBGpBW,cAAsCrB,EACnD,GAAAwF,CAAIT,GACF,MAAMwB,QAAEA,EAAOjD,SAAEA,EAAQkD,WAAEA,GAAezB,EAAKhE,YAC/CxC,KAAK0C,QAAQwF,mBACX,YACA,8BAA8BF,aAAmBjD,gEACZkD,aAAsBzB,EAAKhC,yBAEnE,CAED,MAAA0C,GAC6B,IAAvBlH,KAAKmI,OAAOC,QAEhBpI,KAAK0C,QAAQ2F,eACd,CAED,KAAArB,GACEhH,KAAKmI,OAAOvE,SAASC,GAAUA,EAAMyE,UACtC,CAED,UAAIH,GACF,OAAOnI,KAAK0C,QAAQgB,iBAAiB,wBACtC,IHAD,CACEb,WAAY,0BACZC,sBIxBW,cAAsCrB,EACnDC,cAAgB,CACd6G,MAAOC,OACPC,WAAY,CAAEC,KAAM1F,OAAQ2F,QAAS,OAEvCjH,eAAiB,CAAC,QAAS,WAAY,UAEvC,OAAA6C,GACEvE,KAAK4I,WAAa5I,KAAKmI,OAAOC,MAC/B,CAMD,MAAA7F,CAAOxC,GACL,MAAM8D,EAAQ7D,KAAK6D,MAAM9D,GAazB,OAXI8D,EACFA,EAAMyE,SAENtI,KAAK0C,QAAQwF,mBACX,YACA,8BAA8BlI,KAAK6I,6BAA6B9I,OAIpEC,KAAK4I,WAAa5I,KAAKmI,OAAOC,QAEtBvE,CACT,CAKD,UAAA3B,CAAWnC,GACT,QAASC,KAAK6D,MAAM9D,EACrB,CAED,UAAIoI,GACF,OAAOnI,KAAK0C,QAAQgB,iBAClB,eAAe1D,KAAK6I,sBAEvB,CAED,KAAAhF,CAAM9D,GACJ,OAAOC,KAAK0C,QAAQC,cAClB,eAAe3C,KAAK6I,8BAA8B9I,MAErD,CAED,iBAAA+I,CAAkBP,GAChBvI,KAAK0C,QAAQ2C,gBAAgB,SAAoB,IAAVkD,GACvCvI,KAAK+I,YAAYC,YAAcT,EAC/BvI,KAAKiJ,eAAe5D,gBAAgB,SAAoB,IAAVkD,GAC9CvI,KAAKkJ,aAAa7D,gBAAgB,SAAoB,IAAVkD,EAC7C,IJ9BD,CACE1F,WAAY,0BACZC,sBAAuBtB"}
1
+ {"version":3,"file":"tables.min.js","sources":["../../../javascript/tables/orderable/list_controller.js","../../../javascript/tables/selection/item_controller.js","../../../javascript/tables/application.js","../../../javascript/tables/orderable/item_controller.js","../../../javascript/tables/orderable/form_controller.js","../../../javascript/tables/selection/form_controller.js"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableListController extends Controller {\n static outlets = [\"tables--orderable--item\", \"tables--orderable--form\"];\n\n //region State transitions\n\n startDragging(dragState) {\n this.dragState = dragState;\n\n document.addEventListener(\"mousemove\", this.mousemove);\n document.addEventListener(\"mouseup\", this.mouseup);\n window.addEventListener(\"scroll\", this.scroll, true);\n\n this.element.style.position = \"relative\";\n }\n\n stopDragging() {\n const dragState = this.dragState;\n delete this.dragState;\n\n document.removeEventListener(\"mousemove\", this.mousemove);\n document.removeEventListener(\"mouseup\", this.mouseup);\n window.removeEventListener(\"scroll\", this.scroll, true);\n\n this.element.removeAttribute(\"style\");\n this.tablesOrderableItemOutlets.forEach((item) => item.reset());\n\n return dragState;\n }\n\n drop() {\n // note: early returns guard against turbo updates that prevent us finding\n // the right item to drop on. In this situation it's better to discard the\n // drop than to drop in the wrong place.\n\n const dragItem = this.dragItem;\n\n if (!dragItem) return;\n\n const newIndex = dragItem.dragIndex;\n const targetItem = this.tablesOrderableItemOutlets[newIndex];\n\n if (!targetItem) return;\n\n // swap the dragged item into the correct position for its current offset\n if (newIndex < dragItem.index) {\n targetItem.row.insertAdjacentElement(\"beforebegin\", dragItem.row);\n } else if (newIndex > dragItem.index) {\n targetItem.row.insertAdjacentElement(\"afterend\", dragItem.row);\n }\n\n // reindex all items based on their new positions\n this.tablesOrderableItemOutlets.forEach((item, index) =>\n item.updateIndex(index),\n );\n\n // save the changes\n this.commitChanges();\n }\n\n commitChanges() {\n // clear any existing inputs to prevent duplicates\n this.tablesOrderableFormOutlet.clear();\n\n // insert any items that have changed position\n this.tablesOrderableItemOutlets.forEach((item) => {\n if (item.hasChanges) this.tablesOrderableFormOutlet.add(item);\n });\n\n this.tablesOrderableFormOutlet.submit();\n }\n\n //endregion\n\n //region Events\n\n mousedown(event) {\n if (this.isDragging) return;\n\n const target = this.#targetItem(event.target);\n\n if (!target) return;\n\n event.preventDefault(); // prevent built-in drag\n\n this.startDragging(new DragState(this.element, event, target.id));\n\n this.dragState.updateCursor(this.element, target.row, event, this.animate);\n }\n\n mousemove = (event) => {\n if (!this.isDragging) return;\n\n event.preventDefault(); // prevent build-in drag\n\n if (this.ticking) return;\n\n this.ticking = true;\n\n window.requestAnimationFrame(() => {\n this.ticking = false;\n this.dragState.updateCursor(\n this.element,\n this.dragItem.row,\n event,\n this.animate,\n );\n });\n };\n\n scroll = (event) => {\n if (!this.isDragging || this.ticking) return;\n\n this.ticking = true;\n\n window.requestAnimationFrame(() => {\n this.ticking = false;\n this.dragState.updateScroll(\n this.element,\n this.dragItem.row,\n this.animate,\n );\n });\n };\n\n mouseup = (event) => {\n if (!this.isDragging) return;\n\n this.drop();\n this.stopDragging();\n this.tablesOrderableFormOutlets.forEach((form) => delete form.dragState);\n };\n\n tablesOrderableFormOutletConnected(form, element) {\n if (form.dragState) {\n // restore the previous controller's state\n this.startDragging(form.dragState);\n }\n }\n\n tablesOrderableFormOutletDisconnected(form, element) {\n if (this.isDragging) {\n // cache drag state in the form\n form.dragState = this.stopDragging();\n }\n }\n\n //endregion\n\n //region Helpers\n\n /**\n * Updates the position of the drag item with a relative offset. Updates\n * other items relative to the new position of the drag item, as required.\n *\n * @callback {OrderableListController~animate}\n * @param {number} offset\n */\n animate = (offset) => {\n const dragItem = this.dragItem;\n\n // Visually update the dragItem so it follows the cursor\n dragItem.dragUpdate(offset);\n\n // Visually updates the position of all items in the list relative to the\n // dragged item. No actual changes to orderings at this stage.\n this.#currentItems.forEach((item, index) => {\n if (item === dragItem) return;\n item.updateVisually(index);\n });\n };\n\n get isDragging() {\n return !!this.dragState;\n }\n\n get dragItem() {\n if (!this.isDragging) return null;\n\n return this.tablesOrderableItemOutlets.find(\n (item) => item.id === this.dragState.targetId,\n );\n }\n\n /**\n * Returns the current items in the list, sorted by their current index.\n * Current uses the drag index if the item is being dragged, if set.\n *\n * @returns {Array[OrderableRowController]}\n */\n get #currentItems() {\n return this.tablesOrderableItemOutlets.toSorted(\n (a, b) => a.comparisonIndex - b.comparisonIndex,\n );\n }\n\n /**\n * Returns the item outlet that was clicked on, if any.\n *\n * @param element {HTMLElement} the clicked ordinal cell\n * @returns {OrderableRowController}\n */\n #targetItem(element) {\n return this.tablesOrderableItemOutlets.find(\n (item) => item.element === element,\n );\n }\n\n //endregion\n}\n\n/**\n * During drag we want to be able to translate a document-relative coordinate\n * into a coordinate relative to the list element. This state object calculates\n * and stores internal state so that we can translate absolute page coordinates\n * from mouse events into relative offsets for the list items within the list\n * element.\n *\n * We also keep track of the drag target so that if the controller is attached\n * to a new element during the drag we can continue after the turbo update.\n */\nclass DragState {\n /**\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param event {MouseEvent} the initial event\n * @param id {String} the id of the element being dragged\n */\n constructor(list, event, id) {\n // cursor offset is the offset of the cursor relative to the drag item\n this.cursorOffset = event.offsetY;\n\n // initial offset is the offset position of the drag item at drag start\n this.initialPosition = event.target.offsetTop - list.offsetTop;\n\n // id of the item being dragged\n this.targetId = id;\n }\n\n /**\n * Calculates the offset of the drag item relative to its initial position.\n *\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param row {HTMLElement} the row being dragged\n * @param event {MouseEvent} the current event\n * @param callback {OrderableListController~animate} updates the drag item with a relative offset\n */\n updateCursor(list, row, event, callback) {\n // Calculate and store the list offset relative to the viewport\n // This value is cached so we can calculate the outcome of any scroll events\n this.listOffset = list.getBoundingClientRect().top;\n\n // Calculate the position of the cursor relative to the list.\n // Accounts for scroll offsets by using the item's bounding client rect.\n const cursorPosition = event.clientY - this.listOffset;\n\n // intended item position relative to the list, from cursor position\n let itemPosition = cursorPosition - this.cursorOffset;\n\n this.#updateItemPosition(list, row, itemPosition, callback);\n }\n\n /**\n * Animates the item's position as the list scrolls. Requires a previous call\n * to set the scroll offset.\n *\n * @param list {HTMLElement} the list controller's element (tbody)\n * @param row {HTMLElement} the row being dragged\n * @param callback {OrderableListController~animate} updates the drag item with a relative offset\n */\n updateScroll(list, row, callback) {\n const previousScrollOffset = this.listOffset;\n\n // Calculate and store the list offset relative to the viewport\n // This value is cached so we can calculate the outcome of any scroll events\n this.listOffset = list.getBoundingClientRect().top;\n\n // Calculate the change in scroll offset since the last update\n const scrollDelta = previousScrollOffset - this.listOffset;\n\n // intended item position relative to the list, from cursor position\n const position = this.position + scrollDelta;\n\n this.#updateItemPosition(list, row, position, callback);\n }\n\n #updateItemPosition(list, row, position, callback) {\n // ensure itemPosition is within the bounds of the list (tbody)\n position = Math.max(position, 0);\n position = Math.min(position, list.offsetHeight - row.offsetHeight);\n\n // cache the item's position relative to the list for use in scroll events\n this.position = position;\n\n // Item has position: relative, so we want to calculate the amount to move\n // the item relative to it's DOM position to represent how much it has been\n // dragged by.\n const offset = position - this.initialPosition;\n\n // Convert itemPosition from offset relative to list to offset relative to\n // its position within the DOM (if it hadn't moved).\n callback(offset);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class SelectionItemController extends Controller {\n static outlets = [\"tables--selection--form\"];\n static values = {\n params: Object,\n checked: Boolean,\n };\n\n tablesSelectionFormOutletConnected(form) {\n this.checkedValue = form.isSelected(this.id);\n }\n\n change(e) {\n e.preventDefault();\n\n this.checkedValue = this.tablesSelectionFormOutlet.toggle(this.id);\n }\n\n get id() {\n return this.paramsValue.id;\n }\n\n checkedValueChanged(checked) {\n this.element.querySelector(\"input\").checked = checked;\n }\n}\n","import OrderableItemController from \"./orderable/item_controller\";\nimport OrderableListController from \"./orderable/list_controller\";\nimport OrderableFormController from \"./orderable/form_controller\";\nimport SelectionFormController from \"./selection/form_controller\";\nimport SelectionItemController from \"./selection/item_controller\";\n\nconst Definitions = [\n {\n identifier: \"tables--orderable--item\",\n controllerConstructor: OrderableItemController,\n },\n {\n identifier: \"tables--orderable--list\",\n controllerConstructor: OrderableListController,\n },\n {\n identifier: \"tables--orderable--form\",\n controllerConstructor: OrderableFormController,\n },\n {\n identifier: \"tables--selection--form\",\n controllerConstructor: SelectionFormController,\n },\n {\n identifier: \"tables--selection--item\",\n controllerConstructor: SelectionItemController,\n },\n];\n\nexport { Definitions as default };\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableRowController extends Controller {\n static values = {\n params: Object,\n };\n\n connect() {\n // index from server may be inconsistent with the visual ordering,\n // especially if this is a new node. Use positional indexes instead,\n // as these are the values we will send on save.\n this.index = domIndex(this.row);\n }\n\n paramsValueChanged(params) {\n this.id = params.id_value;\n }\n\n dragUpdate(offset) {\n this.dragOffset = offset;\n this.row.style.position = \"relative\";\n this.row.style.top = offset + \"px\";\n this.row.style.zIndex = \"1\";\n this.row.toggleAttribute(\"dragging\", true);\n }\n\n /**\n * Called on items that are not the dragged item during drag. Updates the\n * visual position of the item relative to the dragged item.\n *\n * @param index {number} intended index of the item during drag\n */\n updateVisually(index) {\n this.row.style.position = \"relative\";\n this.row.style.top = `${\n this.row.offsetHeight * (index - this.dragIndex)\n }px`;\n }\n\n /**\n * Set the index value of the item. This is called on all items after a drop\n * event. If the index is different to the params index then this item has\n * changed.\n *\n * @param index {number} the new index value\n */\n updateIndex(index) {\n this.index = index;\n }\n\n /** Retrieve params for use in the form */\n params(scope) {\n const { id_name, id_value, index_name } = this.paramsValue;\n return [\n { name: `${scope}[${id_value}][${id_name}]`, value: this.id },\n { name: `${scope}[${id_value}][${index_name}]`, value: this.index },\n ];\n }\n\n /**\n * Restore any visual changes made during drag and remove the drag state.\n */\n reset() {\n delete this.dragOffset;\n this.row.removeAttribute(\"style\");\n this.row.removeAttribute(\"dragging\");\n }\n\n /**\n * @returns {boolean} true when the item has a change to its index value\n */\n get hasChanges() {\n return this.paramsValue.index_value !== this.index;\n }\n\n /**\n * Calculate the relative index of the item during drag. This is used to\n * sort items during drag as it takes into account any uncommitted changes\n * to index caused by the drag offset.\n *\n * @returns {number} index for the purposes of drag and drop ordering\n */\n get dragIndex() {\n if (this.dragOffset && this.dragOffset !== 0) {\n return this.index + Math.round(this.dragOffset / this.row.offsetHeight);\n } else {\n return this.index;\n }\n }\n\n /**\n * Index value for use in comparisons during drag. This is used to determine\n * whether the dragged item is above or below another item. If this item is\n * being dragged then we offset the index by 0.5 to ensure that it jumps up\n * or down when it reaches the midpoint of the item above or below it.\n *\n * @returns {number}\n */\n get comparisonIndex() {\n if (this.dragOffset) {\n return this.dragIndex + (this.dragOffset > 0 ? 0.5 : -0.5);\n } else {\n return this.index;\n }\n }\n\n /**\n * The containing row element.\n *\n * @returns {HTMLElement}\n */\n get row() {\n return this.element.parentElement;\n }\n}\n\nfunction domIndex(element) {\n return Array.from(element.parentElement.children).indexOf(element);\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class OrderableFormController extends Controller {\n static values = { scope: String };\n\n add(item) {\n item.params(this.scopeValue).forEach(({ name, value }) => {\n this.element.insertAdjacentHTML(\n \"beforeend\",\n `<input type=\"hidden\" name=\"${name}\" value=\"${value}\" data-generated>`,\n );\n });\n }\n\n submit() {\n if (this.inputs.length === 0) return;\n\n this.element.requestSubmit();\n }\n\n clear() {\n this.inputs.forEach((input) => input.remove());\n }\n\n get inputs() {\n return this.element.querySelectorAll(\"input[data-generated]\");\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class SelectionFormController extends Controller {\n static values = {\n count: Number,\n primaryKey: { type: String, default: \"id\" },\n };\n static targets = [\"count\", \"singular\", \"plural\"];\n\n connect() {\n this.countValue = this.inputs.length;\n }\n\n /**\n * @param id to toggle\n * @return {boolean} true if selected, false if unselected\n */\n toggle(id) {\n const input = this.input(id);\n\n if (input) {\n input.remove();\n } else {\n this.element.insertAdjacentHTML(\n \"beforeend\",\n `<input type=\"hidden\" name=\"${this.primaryKeyValue}[]\" value=\"${id}\">`,\n );\n }\n\n this.countValue = this.inputs.length;\n\n return !input;\n }\n\n /**\n * @returns {boolean} true if the given id is currently selected\n */\n isSelected(id) {\n return !!this.input(id);\n }\n\n get inputs() {\n return this.element.querySelectorAll(\n `input[name=\"${this.primaryKeyValue}[]\"]`,\n );\n }\n\n input(id) {\n return this.element.querySelector(\n `input[name=\"${this.primaryKeyValue}[]\"][value=\"${id}\"]`,\n );\n }\n\n countValueChanged(count) {\n this.element.toggleAttribute(\"hidden\", count === 0);\n this.countTarget.textContent = count;\n this.singularTarget.toggleAttribute(\"hidden\", count !== 1);\n this.pluralTarget.toggleAttribute(\"hidden\", count === 1);\n }\n}\n"],"names":["DragState","constructor","list","event","id","this","cursorOffset","offsetY","initialPosition","target","offsetTop","targetId","updateCursor","row","callback","listOffset","getBoundingClientRect","top","itemPosition","clientY","updateItemPosition","updateScroll","previousScrollOffset","scrollDelta","position","Math","max","min","offsetHeight","SelectionItemController","Controller","static","params","Object","checked","Boolean","tablesSelectionFormOutletConnected","form","checkedValue","isSelected","change","e","preventDefault","tablesSelectionFormOutlet","toggle","paramsValue","checkedValueChanged","element","querySelector","Definitions","identifier","controllerConstructor","connect","index","Array","from","parentElement","children","indexOf","paramsValueChanged","id_value","dragUpdate","offset","dragOffset","style","zIndex","toggleAttribute","updateVisually","dragIndex","updateIndex","scope","id_name","index_name","name","value","reset","removeAttribute","hasChanges","index_value","round","comparisonIndex","startDragging","dragState","document","addEventListener","mousemove","mouseup","window","scroll","stopDragging","removeEventListener","tablesOrderableItemOutlets","forEach","item","drop","dragItem","newIndex","targetItem","insertAdjacentElement","commitChanges","tablesOrderableFormOutlet","clear","add","submit","mousedown","isDragging","animate","ticking","requestAnimationFrame","tablesOrderableFormOutlets","tablesOrderableFormOutletConnected","tablesOrderableFormOutletDisconnected","currentItems","find","toSorted","a","b","String","scopeValue","insertAdjacentHTML","inputs","length","requestSubmit","input","remove","querySelectorAll","count","Number","primaryKey","type","default","countValue","primaryKeyValue","countValueChanged","countTarget","textContent","singularTarget","pluralTarget"],"mappings":"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"}
@@ -13,21 +13,30 @@ module Katalyst
13
13
  @as = as
14
14
  end
15
15
 
16
+ def before_render
17
+ # move @__vc_render_in_block to @row_proc to avoid slot lookup attempting to call it
18
+ @row_proc = @__vc_render_in_block
19
+ @__vc_render_in_block = nil
20
+ end
21
+
16
22
  def model_name
17
23
  collection.model_name if collection.respond_to?(:model_name)
18
24
  end
19
25
 
20
26
  private
21
27
 
28
+ def row_content(row, record)
29
+ @current_row = row
30
+ @current_record = record
31
+ row_proc.call(self, record)
32
+ ensure
33
+ @current_row = nil
34
+ @current_record = nil
35
+ end
36
+
22
37
  def row_proc
23
- if @row_proc
24
- @row_proc
25
- elsif @__vc_render_in_block
26
- @row_proc = @__vc_render_in_block
27
- else
28
- @row_proc = Proc.new do |row, object|
29
- row_renderer.render_row(row, object, view_context)
30
- end
38
+ @row_proc ||= Proc.new do |table, object|
39
+ row_renderer.render_row(table, object, view_context)
31
40
  end
32
41
  end
33
42
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ # Adds dom ids to the table and row components.
6
+ # See [documentation](/docs/identifiable.md) for more details.
7
+ module Identifiable
8
+ extend ActiveSupport::Concern
9
+
10
+ module Defaults
11
+ extend self
12
+
13
+ # Returns the default dom id for the table, uses the collection's
14
+ # model name's route_key as a sensible default.
15
+ def default_table_id(collection = self.collection)
16
+ collection.model_name.route_key
17
+ end
18
+ end
19
+
20
+ included do
21
+ include Defaults
22
+ end
23
+
24
+ def initialize(generate_ids: false, **)
25
+ super(**)
26
+
27
+ @generate_ids = generate_ids
28
+ end
29
+
30
+ def identifiable?
31
+ @generate_ids
32
+ end
33
+
34
+ def id
35
+ html_attributes[:id]
36
+ end
37
+
38
+ def before_render
39
+ if identifiable?
40
+ update_html_attributes(id: default_table_id(collection)) if id.nil?
41
+
42
+ @body_row_callbacks << Proc.new do |row, record|
43
+ row.update_html_attributes(id: dom_id(record))
44
+ end
45
+ end
46
+
47
+ super
48
+ end
49
+ end
50
+ end
51
+ end
@@ -11,122 +11,52 @@ module Katalyst
11
11
  ITEM_CONTROLLER = "tables--orderable--item"
12
12
  LIST_CONTROLLER = "tables--orderable--list"
13
13
 
14
- using Katalyst::HtmlAttributes::HasHtmlAttributes
15
-
16
- # Support for inclusion in a table component class
17
- # Adds an `orderable` slot and component configuration
18
- included do
19
- # Add `orderable` slot to table component
20
- config_component :orderable, default: "Katalyst::Tables::Orderable::FormComponent"
21
- renders_one(:orderable, lambda do |**attrs|
22
- orderable_component.new(table: self, **attrs)
23
- end)
14
+ # Returns the default dom id for the selection form, uses the table's
15
+ # default id with '_selection' appended.
16
+ def self.default_form_id(collection)
17
+ "#{Identifiable::Defaults.default_table_id(collection)}_order_form"
24
18
  end
25
19
 
26
- # Support for extending a table component instance
27
- # Adds methods to the table component instance
28
- def self.extended(table)
29
- table.extend(TableMethods)
30
-
31
- # ensure row components support orderable column calls
32
- table.send(:add_orderable_columns)
20
+ # Generate a form input 'name' for param updates for a given record and attribute.
21
+ def self.default_scope(collection)
22
+ "order[#{collection.model_name.plural}]"
33
23
  end
34
24
 
35
- def initialize(**attributes)
36
- super
37
-
38
- # ensure row components support orderable column calls
39
- add_orderable_columns
25
+ # Generate a nested scope for for param updates for a given record and attribute.
26
+ # Will be concatenated with the form's scope in the browser.
27
+ def self.record_scope(id, attribute)
28
+ "[#{id}][#{attribute}]"
40
29
  end
41
30
 
42
- def tbody_attributes
43
- return super unless orderable?
31
+ # Generates a column for the user to drag and drop to reorder data rows.
32
+ #
33
+ # @param column [Symbol] the value to update when the user reorders the rows
34
+ # @param primary_key [Symbol] key for identifying rows that have changed in params (:id by default)
35
+ # @param ** [Hash] HTML attributes to be added to column cells
36
+ # @param & [Proc] optional block to wrap the cell content
37
+ # @return [void]
38
+ #
39
+ # @example Render a column with a drag-and-drop handle for users to reorder rows
40
+ # <% row.ordinal %> # label => <th></th>, data => <td ...>⠿</td>
41
+ def ordinal(column = :ordinal, primary_key: :id, **, &)
42
+ initialize_orderable if row.header?
44
43
 
45
- super.merge_html(
46
- { data: { controller: LIST_CONTROLLER,
47
- action: <<~ACTIONS.squish,
48
- mousedown->#{LIST_CONTROLLER}#mousedown
49
- ACTIONS
50
- "#{LIST_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{orderable.id}",
51
- "#{LIST_CONTROLLER}-#{ITEM_CONTROLLER}-outlet" => "td.ordinal" } },
52
- )
44
+ with_cell(Cells::OrdinalComponent.new(
45
+ collection:, row:, column:, record:, label: "", heading: false, primary_key:, **,
46
+ ), &)
53
47
  end
54
48
 
55
49
  private
56
50
 
57
- # Add `orderable` columns to row components
58
- def add_orderable_columns
59
- header_row_component.include(HeaderRow)
60
- body_row_component.include(BodyRow)
61
- end
62
-
63
- # Methods required to emulate a slot when extending an existing table.
64
- module TableMethods
65
- def with_orderable(**attrs)
66
- @orderable = FormComponent.new(table: self, **attrs)
67
-
68
- self
69
- end
70
-
71
- def orderable?
72
- @orderable.present?
73
- end
74
-
75
- def orderable
76
- @orderable
77
- end
78
- end
79
-
80
- module HeaderRow # :nodoc:
81
- def ordinal(attribute = :ordinal, **)
82
- cell(attribute, class: "ordinal", label: "")
83
- end
84
- end
85
-
86
- module BodyRow # :nodoc:
87
- def ordinal(attribute = :ordinal, primary_key: :id)
88
- id = @record.public_send(primary_key)
89
- params = {
90
- id_name: @table.orderable.record_scope(id, primary_key),
91
- id_value: id,
92
- index_name: @table.orderable.record_scope(id, attribute),
93
- index_value: @record.public_send(attribute),
94
- }
95
- cell(attribute, class: "ordinal", draggable: true, data: {
96
- controller: ITEM_CONTROLLER,
97
- "#{ITEM_CONTROLLER}-params-value": params.to_json,
98
- }) { t("katalyst.tables.orderable.value") }
99
- end
100
- end
101
-
102
- class FormComponent < ViewComponent::Base # :nodoc:
103
- attr_reader :id, :url, :scope
104
-
105
- def initialize(table:,
106
- url:,
107
- id: "#{table.id}-orderable-form",
108
- scope: "order[#{table.collection.model_name.plural}]")
109
- super
110
-
111
- @table = table
112
- @id = id
113
- @url = url
114
- @scope = scope
115
- end
116
-
117
- def record_scope(id, attribute)
118
- "#{scope}[#{id}][#{attribute}]"
119
- end
120
-
121
- def call
122
- form_with(id:, url:, method: :patch, data: { controller: FORM_CONTROLLER }) do |form|
123
- form.button(hidden: "")
124
- end
125
- end
126
-
127
- def inspect
128
- "#<#{self.class.name} id: #{id.inspect}, url: #{url.inspect}, scope: #{scope.inspect}>"
129
- end
51
+ def initialize_orderable
52
+ update_tbody_attributes(
53
+ data: {
54
+ controller: LIST_CONTROLLER,
55
+ action: "mousedown->#{LIST_CONTROLLER}#mousedown",
56
+ "#{LIST_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{Orderable.default_form_id(collection)}",
57
+ "#{LIST_CONTROLLER}-#{ITEM_CONTROLLER}-outlet" => "td.ordinal",
58
+ },
59
+ )
130
60
  end
131
61
  end
132
62
  end
@@ -10,83 +10,26 @@ module Katalyst
10
10
  FORM_CONTROLLER = "tables--selection--form"
11
11
  ITEM_CONTROLLER = "tables--selection--item"
12
12
 
13
- using Katalyst::HtmlAttributes::HasHtmlAttributes
14
-
15
- # Support for inclusion in a table component class
16
- # Adds an `selectable` slot and component configuration
17
- included do
18
- # Add `selectable` slot to table component
19
- config_component :selection, default: "Katalyst::Tables::Selectable::FormComponent"
20
- renders_one(:selection, lambda do |**attrs|
21
- selection_component.new(table: self, **attrs)
22
- end)
23
- end
24
-
25
- # Support for extending a table component instance
26
- # Adds methods to the table component instance
27
- def self.extended(table)
28
- table.extend(TableMethods)
29
-
30
- # ensure row components support selectable column calls
31
- table.send(:add_selectable_columns)
32
- end
33
-
34
- def initialize(**attributes)
35
- super
36
-
37
- # ensure row components support selectable column calls
38
- add_selectable_columns
39
- end
40
-
41
- private
42
-
43
- # Add `selectable` columns to row components
44
- def add_selectable_columns
45
- header_row_component.include(HeaderRow)
46
- body_row_component.include(BodyRow)
47
- end
48
-
49
- # Methods required to emulate a slot when extending an existing table.
50
- module TableMethods
51
- def with_selection(**attrs)
52
- @selection = FormComponent.new(table: self, **attrs)
53
-
54
- self
55
- end
56
-
57
- def selectable?
58
- @selection.present?
59
- end
60
-
61
- def selection
62
- @selection ||= FormComponent.new(table: self)
63
- end
64
- end
65
-
66
- module HeaderRow # :nodoc:
67
- def selection
68
- cell(:_selection, class: "selection", label: "")
69
- end
13
+ # Returns the default dom id for the selection form, uses the table's
14
+ # default id with '_selection' appended.
15
+ def self.default_form_id(collection)
16
+ "#{Identifiable::Defaults.default_table_id(collection)}_selection_form"
70
17
  end
71
18
 
72
- module BodyRow # :nodoc:
73
- def selection
74
- id = @record.public_send(@table.selection.primary_key)
75
- params = {
76
- id:,
77
- }
78
- cell(:_selection,
79
- class: "selection",
80
- data: {
81
- controller: ITEM_CONTROLLER,
82
- "#{ITEM_CONTROLLER}-params-value" => params.to_json,
83
- "#{ITEM_CONTROLLER}-#{FORM_CONTROLLER}-outlet" => "##{@table.selection.id}",
84
- action: "change->#{ITEM_CONTROLLER}#change",
85
- turbo_permanent: "",
86
- }) do
87
- tag.input(type: :checkbox)
88
- end
89
- end
19
+ # Adds the selection column to the table
20
+ #
21
+ # @param params [Hash] params to pass to the controller for selected rows
22
+ # @param form_id [String] id of the form element that will submit the selected row params
23
+ # @param ** [Hash] HTML attributes to be added to column cells
24
+ # @param & [Proc] optional block to alter the cell content
25
+ # @return [void]
26
+ #
27
+ # @example Render a select column
28
+ # <% row.select %> # => <td><input type="checkbox" ...></td>
29
+ def select(params: { id: record&.id }, form_id: Selectable.default_form_id(collection), **, &)
30
+ with_cell(Cells::SelectComponent.new(
31
+ collection:, row:, column: :_select, record:, label: "", heading: false, params:, form_id:, **,
32
+ ), &)
90
33
  end
91
34
  end
92
35
  end
@@ -7,25 +7,59 @@ module Katalyst
7
7
  module Sortable
8
8
  extend ActiveSupport::Concern
9
9
 
10
- # Returns true when the given attribute is sortable.
11
- def sortable?(attribute)
12
- sorting&.supports?(collection, attribute)
10
+ def initialize(**)
11
+ super(**)
12
+
13
+ @header_row_cell_callbacks << method(:add_sorting_to_cell) if collection.sortable?
14
+ end
15
+
16
+ private
17
+
18
+ def add_sorting_to_cell(cell)
19
+ if collection.sortable?(cell.column)
20
+ cell.update_html_attributes(data: { sort: collection.sort_status(cell.column) })
21
+ cell.with_content_wrapper(SortableHeaderComponent.new(collection:, cell:))
22
+ end
13
23
  end
14
24
 
15
- # Generates a url for applying/toggling sort for the given column.
16
- def sort_url(attribute) # rubocop:disable Metrics/AbcSize
17
- # Implementation inspired by pagy's `pagy_url_for` helper.
18
- # Preserve any existing GET parameters
19
- # CAUTION: these parameters are not sanitised
20
- sort = attribute && sorting.toggle(attribute)
21
- params = if sort && !sort.eql?(sorting.default)
22
- request.GET.merge("sort" => sort).except("page")
23
- else
24
- request.GET.except("page", "sort")
25
- end
26
- query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
27
-
28
- "#{request.path}#{query_string}"
25
+ class SortableHeaderComponent < ViewComponent::Base
26
+ include Katalyst::HtmlAttributes
27
+
28
+ attr_reader :collection, :cell
29
+
30
+ delegate :column, to: :cell
31
+
32
+ def initialize(collection:, cell:, **)
33
+ super(**)
34
+
35
+ @collection = collection
36
+ @cell = cell
37
+ end
38
+
39
+ def call
40
+ link_to(content, sort_url, **html_attributes)
41
+ end
42
+
43
+ # Generates a url for applying/toggling sort for the given column.
44
+ def sort_url
45
+ # rubocop:disable Metrics/AbcSize
46
+ # Implementation inspired by pagy's `pagy_url_for` helper.
47
+ # Preserve any existing GET parameters
48
+ # CAUTION: these parameters are not sanitised
49
+ sort = column && collection.toggle_sort(column)
50
+ params = if sort && !sort.eql?(collection.default_sort)
51
+ request.GET.merge("sort" => sort).except("page")
52
+ else
53
+ request.GET.except("page", "sort")
54
+ end
55
+ query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
56
+
57
+ "#{request.path}#{query_string}"
58
+ end
59
+
60
+ def default_html_attributes
61
+ { data: { turbo_action: "replace" } }
62
+ end
29
63
  end
30
64
  end
31
65
  end
@@ -1,13 +1,13 @@
1
1
  <%= tag.table(**html_attributes) do %>
2
2
  <%= render caption if caption? %>
3
- <% if header? %>
3
+ <% if header_row? %>
4
4
  <%= tag.thead(**thead_attributes) do %>
5
- <%= header_row.render_in(view_context, &row_proc) %>
5
+ <%= header_row %>
6
6
  <% end %>
7
7
  <% end %>
8
8
  <%= tag.tbody(**tbody_attributes) do %>
9
- <% collection.each do |record| %>
10
- <%= body_row(record).render_in(view_context) { |r| row_proc.call(r, record) } %>
9
+ <% body_rows.each do |body_row| %>
10
+ <%= body_row %>
11
11
  <% end %>
12
12
  <% end %>
13
13
  <% end %>