katalyst-tables 2.4.0 → 2.6.0.beta

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5324d0bccadf307f48cb2199b8ba9123cb519756dda81cfc0c4c88a4abdfd886
4
- data.tar.gz: ad299f1c8f62454aa3433b00cf4c0b69dacfe59cd6cfe598ab3ea2969cedecde
3
+ metadata.gz: 10815e05f743cbe5d5c21845bf15e94318b2f0302c5466f55dc8094a34330fc9
4
+ data.tar.gz: 0ded18dae608cee47f68ac143e55a63a58c7c36b5c2bfeb10add6ebd67771f32
5
5
  SHA512:
6
- metadata.gz: bd4daa1c09acaab6e85cbf787cfafb51303c9f030f26f15ca5f0a593da02c5dd526bf73d07b5cf6a99bfc267f2ea2a17d837e5eae81d607df2ad7788a9a99260
7
- data.tar.gz: 143552907f2bf01ad256fc6e24b06404db1f173fe2ce3f4dca802eacd903fc5ab2f950a7c8c9ff6b086efe00c8838b8eca9dfbb2edcc0cc61d76717f6ca171ac
6
+ metadata.gz: 4dc10673cee47c520e1b15f564f5ea532510d54923b97e13c5e49559fe2c474cb962b8955102182b2874889127012d74794efa99a9494aec1fedc0532ddea02a
7
+ data.tar.gz: 2f65c7295d432fb3ada59eeb6d0c1dbc50aee134613ae922b583e738a5c7adc3fa83e1f3edc8833b96ae6642e6c88d0b492039cc0f30a72178997be211604d7f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Added table row selection
4
+ - See [[docs/selectable.md]] for examples
5
+
6
+ ## [2.5.0]
7
+
8
+ - Breaking change: use Rails' object lookup path to find row partials
9
+ Previously: Nested::ResourceController would have looked for Nested::Model in
10
+ the controller directory:
11
+ app/views/nested/resources/_nested_model.html+row.erb
12
+ After this change, uses Rails' polymorphic partials logic and looks in the
13
+ model views directory:
14
+ app/views/nested/models/_model.html+row.erb
15
+
3
16
  ## [2.4.0]
4
17
 
5
18
  - Internal refactor of filters to make it easier to add custom extensions
@@ -474,6 +474,91 @@ class OrderableFormController extends Controller {
474
474
  }
475
475
  }
476
476
 
477
+ class SelectionFormController extends Controller {
478
+ static values = {
479
+ count: Number,
480
+ primaryKey: { type: String, default: "id" },
481
+ };
482
+ static targets = ["count", "singular", "plural"];
483
+
484
+ connect() {
485
+ this.countValue = this.inputs.length;
486
+ }
487
+
488
+ /**
489
+ * @param id to toggle
490
+ * @return {boolean} true if selected, false if unselected
491
+ */
492
+ toggle(id) {
493
+ const input = this.input(id);
494
+
495
+ if (input) {
496
+ input.remove();
497
+ } else {
498
+ this.element.insertAdjacentHTML(
499
+ "beforeend",
500
+ `<input type="hidden" name="${this.primaryKeyValue}[]" value="${id}">`,
501
+ );
502
+ }
503
+
504
+ this.countValue = this.inputs.length;
505
+
506
+ return !input;
507
+ }
508
+
509
+ /**
510
+ * @returns {boolean} true if the given id is currently selected
511
+ */
512
+ isSelected(id) {
513
+ return !!this.input(id);
514
+ }
515
+
516
+ get inputs() {
517
+ return this.element.querySelectorAll(
518
+ `input[name="${this.primaryKeyValue}[]"]`,
519
+ );
520
+ }
521
+
522
+ input(id) {
523
+ return this.element.querySelector(
524
+ `input[name="${this.primaryKeyValue}[]"][value="${id}"]`,
525
+ );
526
+ }
527
+
528
+ countValueChanged(count) {
529
+ this.element.toggleAttribute("hidden", count === 0);
530
+ this.countTarget.textContent = count;
531
+ this.singularTarget.toggleAttribute("hidden", count !== 1);
532
+ this.pluralTarget.toggleAttribute("hidden", count === 1);
533
+ }
534
+ }
535
+
536
+ class SelectionItemController extends Controller {
537
+ static outlets = ["tables--selection--form"];
538
+ static values = {
539
+ params: Object,
540
+ checked: Boolean,
541
+ };
542
+
543
+ tablesSelectionFormOutletConnected(form) {
544
+ this.checkedValue = form.isSelected(this.id);
545
+ }
546
+
547
+ change(e) {
548
+ e.preventDefault();
549
+
550
+ this.checkedValue = this.tablesSelectionFormOutlet.toggle(this.id);
551
+ }
552
+
553
+ get id() {
554
+ return this.paramsValue.id;
555
+ }
556
+
557
+ checkedValueChanged(checked) {
558
+ this.element.querySelector("input").checked = checked;
559
+ }
560
+ }
561
+
477
562
  const Definitions = [
478
563
  {
479
564
  identifier: "tables--turbo--collection",
@@ -491,6 +576,14 @@ const Definitions = [
491
576
  identifier: "tables--orderable--form",
492
577
  controllerConstructor: OrderableFormController,
493
578
  },
579
+ {
580
+ identifier: "tables--selection--form",
581
+ controllerConstructor: SelectionFormController,
582
+ },
583
+ {
584
+ identifier: "tables--selection--item",
585
+ controllerConstructor: SelectionItemController,
586
+ },
494
587
  ];
495
588
 
496
589
  export { Definitions as default };
@@ -474,6 +474,91 @@ class OrderableFormController extends Controller {
474
474
  }
475
475
  }
476
476
 
477
+ class SelectionFormController extends Controller {
478
+ static values = {
479
+ count: Number,
480
+ primaryKey: { type: String, default: "id" },
481
+ };
482
+ static targets = ["count", "singular", "plural"];
483
+
484
+ connect() {
485
+ this.countValue = this.inputs.length;
486
+ }
487
+
488
+ /**
489
+ * @param id to toggle
490
+ * @return {boolean} true if selected, false if unselected
491
+ */
492
+ toggle(id) {
493
+ const input = this.input(id);
494
+
495
+ if (input) {
496
+ input.remove();
497
+ } else {
498
+ this.element.insertAdjacentHTML(
499
+ "beforeend",
500
+ `<input type="hidden" name="${this.primaryKeyValue}[]" value="${id}">`,
501
+ );
502
+ }
503
+
504
+ this.countValue = this.inputs.length;
505
+
506
+ return !input;
507
+ }
508
+
509
+ /**
510
+ * @returns {boolean} true if the given id is currently selected
511
+ */
512
+ isSelected(id) {
513
+ return !!this.input(id);
514
+ }
515
+
516
+ get inputs() {
517
+ return this.element.querySelectorAll(
518
+ `input[name="${this.primaryKeyValue}[]"]`,
519
+ );
520
+ }
521
+
522
+ input(id) {
523
+ return this.element.querySelector(
524
+ `input[name="${this.primaryKeyValue}[]"][value="${id}"]`,
525
+ );
526
+ }
527
+
528
+ countValueChanged(count) {
529
+ this.element.toggleAttribute("hidden", count === 0);
530
+ this.countTarget.textContent = count;
531
+ this.singularTarget.toggleAttribute("hidden", count !== 1);
532
+ this.pluralTarget.toggleAttribute("hidden", count === 1);
533
+ }
534
+ }
535
+
536
+ class SelectionItemController extends Controller {
537
+ static outlets = ["tables--selection--form"];
538
+ static values = {
539
+ params: Object,
540
+ checked: Boolean,
541
+ };
542
+
543
+ tablesSelectionFormOutletConnected(form) {
544
+ this.checkedValue = form.isSelected(this.id);
545
+ }
546
+
547
+ change(e) {
548
+ e.preventDefault();
549
+
550
+ this.checkedValue = this.tablesSelectionFormOutlet.toggle(this.id);
551
+ }
552
+
553
+ get id() {
554
+ return this.paramsValue.id;
555
+ }
556
+
557
+ checkedValueChanged(checked) {
558
+ this.element.querySelector("input").checked = checked;
559
+ }
560
+ }
561
+
477
562
  const Definitions = [
478
563
  {
479
564
  identifier: "tables--turbo--collection",
@@ -491,6 +576,14 @@ const Definitions = [
491
576
  identifier: "tables--orderable--form",
492
577
  controllerConstructor: OrderableFormController,
493
578
  },
579
+ {
580
+ identifier: "tables--selection--form",
581
+ controllerConstructor: SelectionFormController,
582
+ },
583
+ {
584
+ identifier: "tables--selection--item",
585
+ controllerConstructor: SelectionItemController,
586
+ },
494
587
  ];
495
588
 
496
589
  export { Definitions as default };
@@ -1,2 +1,2 @@
1
- import{Controller as t}from"@hotwired/stimulus";import{Turbo as e}from"@hotwired/turbo-rails";class s{constructor(t,e,s){this.cursorOffset=e.offsetY,this.initialPosition=e.target.offsetTop-t.offsetTop,this.targetId=s}updateCursor(t,e,s,i){this.listOffset=t.getBoundingClientRect().top;let r=s.clientY-this.listOffset-this.cursorOffset;this.#t(t,e,r,i)}updateScroll(t,e,s){const i=this.listOffset;this.listOffset=t.getBoundingClientRect().top;const r=i-this.listOffset,a=this.position+r;this.#t(t,e,a,s)}#t(t,e,s,i){s=Math.max(s,0),s=Math.min(s,t.offsetHeight-e.offsetHeight),this.position=s;i(s-this.initialPosition)}}const i=[{identifier:"tables--turbo--collection",controllerConstructor:class extends t{static values={query:String,sort:String};queryValueChanged(t){e.navigator.history.replace(this.#e(t))}sortValueChanged(t){document.querySelectorAll(this.#s).forEach((e=>{e&&(e.value=t)}))}get#s(){return"input[name='sort']"}#e(t){const e=this.element.closest("turbo-frame");let s;return s=e?new URL(e.baseURI):new URL(window.location.href),s.search=t,s}}},{identifier:"tables--orderable--item",controllerConstructor:class extends t{static values={params:Object};connect(){var t;this.index=(t=this.row,Array.from(t.parentElement.children).indexOf(t))}paramsValueChanged(t){this.id=t.id_value}dragUpdate(t){this.dragOffset=t,this.row.style.position="relative",this.row.style.top=t+"px",this.row.style.zIndex="1",this.row.toggleAttribute("dragging",!0)}updateVisually(t){this.row.style.position="relative",this.row.style.top=this.row.offsetHeight*(t-this.dragIndex)+"px"}updateIndex(t){this.index=t}reset(){delete this.dragOffset,this.row.removeAttribute("style"),this.row.removeAttribute("dragging")}get hasChanges(){return this.paramsValue.index_value!==this.index}get dragIndex(){return this.dragOffset&&0!==this.dragOffset?this.index+Math.round(this.dragOffset/this.row.offsetHeight):this.index}get comparisonIndex(){return this.dragOffset?this.dragIndex+(this.dragOffset>0?.5:-.5):this.index}get row(){return this.element.parentElement}}},{identifier:"tables--orderable--list",controllerConstructor:class extends t{static outlets=["tables--orderable--item","tables--orderable--form"];startDragging(t){this.dragState=t,document.addEventListener("mousemove",this.mousemove),document.addEventListener("mouseup",this.mouseup),window.addEventListener("scroll",this.scroll,!0),this.element.style.position="relative"}stopDragging(){const t=this.dragState;return delete this.dragState,document.removeEventListener("mousemove",this.mousemove),document.removeEventListener("mouseup",this.mouseup),window.removeEventListener("scroll",this.scroll,!0),this.element.removeAttribute("style"),this.tablesOrderableItemOutlets.forEach((t=>t.reset())),t}drop(){const t=this.dragItem;if(!t)return;const e=t.dragIndex,s=this.tablesOrderableItemOutlets[e];s&&(e<t.index?s.row.insertAdjacentElement("beforebegin",t.row):e>t.index&&s.row.insertAdjacentElement("afterend",t.row),this.tablesOrderableItemOutlets.forEach(((t,e)=>t.updateIndex(e))),this.commitChanges())}commitChanges(){this.tablesOrderableFormOutlet.clear(),this.tablesOrderableItemOutlets.forEach((t=>{t.hasChanges&&this.tablesOrderableFormOutlet.add(t)})),this.tablesOrderableFormOutlet.submit()}mousedown(t){if(this.isDragging)return;const e=this.#i(t.target);e&&(t.preventDefault(),this.startDragging(new s(this.element,t,e.id)),this.dragState.updateCursor(this.element,e.row,t,this.animate))}mousemove=t=>{this.isDragging&&(t.preventDefault(),this.ticking||(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateCursor(this.element,this.dragItem.row,t,this.animate)}))))};scroll=t=>{this.isDragging&&!this.ticking&&(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateScroll(this.element,this.dragItem.row,this.animate)})))};mouseup=t=>{this.isDragging&&(this.drop(),this.stopDragging(),this.tablesOrderableFormOutlets.forEach((t=>delete t.dragState)))};tablesOrderableFormOutletConnected(t,e){t.dragState&&this.startDragging(t.dragState)}tablesOrderableFormOutletDisconnected(t,e){this.isDragging&&(t.dragState=this.stopDragging())}animate=t=>{const e=this.dragItem;e.dragUpdate(t),this.#r.forEach(((t,s)=>{t!==e&&t.updateVisually(s)}))};get isDragging(){return!!this.dragState}get dragItem(){return this.isDragging?this.tablesOrderableItemOutlets.find((t=>t.id===this.dragState.targetId)):null}get#r(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#i(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{add(t){const{id_name:e,id_value:s,index_name:i}=t.paramsValue;this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${e}" value="${s}" data-generated>\n <input type="hidden" name="${i}" value="${t.index}" data-generated>`)}submit(){0!==this.inputs.length&&this.element.requestSubmit()}clear(){this.inputs.forEach((t=>t.remove()))}get inputs(){return this.element.querySelectorAll("input[data-generated]")}}}];export{i as default};
1
+ import{Controller as t}from"@hotwired/stimulus";import{Turbo as e}from"@hotwired/turbo-rails";class s{constructor(t,e,s){this.cursorOffset=e.offsetY,this.initialPosition=e.target.offsetTop-t.offsetTop,this.targetId=s}updateCursor(t,e,s,i){this.listOffset=t.getBoundingClientRect().top;let r=s.clientY-this.listOffset-this.cursorOffset;this.#t(t,e,r,i)}updateScroll(t,e,s){const i=this.listOffset;this.listOffset=t.getBoundingClientRect().top;const r=i-this.listOffset,a=this.position+r;this.#t(t,e,a,s)}#t(t,e,s,i){s=Math.max(s,0),s=Math.min(s,t.offsetHeight-e.offsetHeight),this.position=s;i(s-this.initialPosition)}}class i extends t{static outlets=["tables--selection--form"];static values={params:Object,checked:Boolean};tablesSelectionFormOutletConnected(t){this.checkedValue=t.isSelected(this.id)}change(t){t.preventDefault(),this.checkedValue=this.tablesSelectionFormOutlet.toggle(this.id)}get id(){return this.paramsValue.id}checkedValueChanged(t){this.element.querySelector("input").checked=t}}const r=[{identifier:"tables--turbo--collection",controllerConstructor:class extends t{static values={query:String,sort:String};queryValueChanged(t){e.navigator.history.replace(this.#e(t))}sortValueChanged(t){document.querySelectorAll(this.#s).forEach((e=>{e&&(e.value=t)}))}get#s(){return"input[name='sort']"}#e(t){const e=this.element.closest("turbo-frame");let s;return s=e?new URL(e.baseURI):new URL(window.location.href),s.search=t,s}}},{identifier:"tables--orderable--item",controllerConstructor:class extends t{static values={params:Object};connect(){var t;this.index=(t=this.row,Array.from(t.parentElement.children).indexOf(t))}paramsValueChanged(t){this.id=t.id_value}dragUpdate(t){this.dragOffset=t,this.row.style.position="relative",this.row.style.top=t+"px",this.row.style.zIndex="1",this.row.toggleAttribute("dragging",!0)}updateVisually(t){this.row.style.position="relative",this.row.style.top=this.row.offsetHeight*(t-this.dragIndex)+"px"}updateIndex(t){this.index=t}reset(){delete this.dragOffset,this.row.removeAttribute("style"),this.row.removeAttribute("dragging")}get hasChanges(){return this.paramsValue.index_value!==this.index}get dragIndex(){return this.dragOffset&&0!==this.dragOffset?this.index+Math.round(this.dragOffset/this.row.offsetHeight):this.index}get comparisonIndex(){return this.dragOffset?this.dragIndex+(this.dragOffset>0?.5:-.5):this.index}get row(){return this.element.parentElement}}},{identifier:"tables--orderable--list",controllerConstructor:class extends t{static outlets=["tables--orderable--item","tables--orderable--form"];startDragging(t){this.dragState=t,document.addEventListener("mousemove",this.mousemove),document.addEventListener("mouseup",this.mouseup),window.addEventListener("scroll",this.scroll,!0),this.element.style.position="relative"}stopDragging(){const t=this.dragState;return delete this.dragState,document.removeEventListener("mousemove",this.mousemove),document.removeEventListener("mouseup",this.mouseup),window.removeEventListener("scroll",this.scroll,!0),this.element.removeAttribute("style"),this.tablesOrderableItemOutlets.forEach((t=>t.reset())),t}drop(){const t=this.dragItem;if(!t)return;const e=t.dragIndex,s=this.tablesOrderableItemOutlets[e];s&&(e<t.index?s.row.insertAdjacentElement("beforebegin",t.row):e>t.index&&s.row.insertAdjacentElement("afterend",t.row),this.tablesOrderableItemOutlets.forEach(((t,e)=>t.updateIndex(e))),this.commitChanges())}commitChanges(){this.tablesOrderableFormOutlet.clear(),this.tablesOrderableItemOutlets.forEach((t=>{t.hasChanges&&this.tablesOrderableFormOutlet.add(t)})),this.tablesOrderableFormOutlet.submit()}mousedown(t){if(this.isDragging)return;const e=this.#i(t.target);e&&(t.preventDefault(),this.startDragging(new s(this.element,t,e.id)),this.dragState.updateCursor(this.element,e.row,t,this.animate))}mousemove=t=>{this.isDragging&&(t.preventDefault(),this.ticking||(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateCursor(this.element,this.dragItem.row,t,this.animate)}))))};scroll=t=>{this.isDragging&&!this.ticking&&(this.ticking=!0,window.requestAnimationFrame((()=>{this.ticking=!1,this.dragState.updateScroll(this.element,this.dragItem.row,this.animate)})))};mouseup=t=>{this.isDragging&&(this.drop(),this.stopDragging(),this.tablesOrderableFormOutlets.forEach((t=>delete t.dragState)))};tablesOrderableFormOutletConnected(t,e){t.dragState&&this.startDragging(t.dragState)}tablesOrderableFormOutletDisconnected(t,e){this.isDragging&&(t.dragState=this.stopDragging())}animate=t=>{const e=this.dragItem;e.dragUpdate(t),this.#r.forEach(((t,s)=>{t!==e&&t.updateVisually(s)}))};get isDragging(){return!!this.dragState}get dragItem(){return this.isDragging?this.tablesOrderableItemOutlets.find((t=>t.id===this.dragState.targetId)):null}get#r(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#i(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{add(t){const{id_name:e,id_value:s,index_name:i}=t.paramsValue;this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${e}" value="${s}" data-generated>\n <input type="hidden" name="${i}" value="${t.index}" data-generated>`)}submit(){0!==this.inputs.length&&this.element.requestSubmit()}clear(){this.inputs.forEach((t=>t.remove()))}get inputs(){return this.element.querySelectorAll("input[data-generated]")}}},{identifier:"tables--selection--form",controllerConstructor:class extends t{static values={count:Number,primaryKey:{type:String,default:"id"}};static targets=["count","singular","plural"];connect(){this.countValue=this.inputs.length}toggle(t){const e=this.input(t);return e?e.remove():this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${this.primaryKeyValue}[]" value="${t}">`),this.countValue=this.inputs.length,!e}isSelected(t){return!!this.input(t)}get inputs(){return this.element.querySelectorAll(`input[name="${this.primaryKeyValue}[]"]`)}input(t){return this.element.querySelector(`input[name="${this.primaryKeyValue}[]"][value="${t}"]`)}countValueChanged(t){this.element.toggleAttribute("hidden",0===t),this.countTarget.textContent=t,this.singularTarget.toggleAttribute("hidden",1!==t),this.pluralTarget.toggleAttribute("hidden",1===t)}}},{identifier:"tables--selection--item",controllerConstructor:i}];export{r as default};
2
2
  //# sourceMappingURL=tables.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tables.min.js","sources":["../../../javascript/tables/orderable/list_controller.js","../../../javascript/tables/application.js","../../../javascript/tables/turbo/collection_controller.js","../../../javascript/tables/orderable/item_controller.js","../../../javascript/tables/orderable/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 TurboCollectionController from \"./turbo/collection_controller\";\nimport ItemController from \"./orderable/item_controller\";\nimport ListController from \"./orderable/list_controller\";\nimport FormController from \"./orderable/form_controller\";\n\nconst Definitions = [\n {\n identifier: \"tables--turbo--collection\",\n controllerConstructor: TurboCollectionController,\n },\n {\n identifier: \"tables--orderable--item\",\n controllerConstructor: ItemController,\n },\n {\n identifier: \"tables--orderable--list\",\n controllerConstructor: ListController,\n },\n {\n identifier: \"tables--orderable--form\",\n controllerConstructor: FormController,\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"],"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","Definitions","identifier","controllerConstructor","Controller","static","query","String","sort","queryValueChanged","Turbo","navigator","history","replace","url","sortValueChanged","document","querySelectorAll","sortSelector","forEach","input","value","frame","element","closest","URL","baseURI","window","location","href","search","params","Object","connect","index","Array","from","parentElement","children","indexOf","paramsValueChanged","id_value","dragUpdate","offset","dragOffset","style","zIndex","toggleAttribute","updateVisually","dragIndex","updateIndex","reset","removeAttribute","hasChanges","paramsValue","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","preventDefault","animate","ticking","requestAnimationFrame","tablesOrderableFormOutlets","form","tablesOrderableFormOutletConnected","tablesOrderableFormOutletDisconnected","currentItems","find","toSorted","a","b","id_name","index_name","insertAdjacentHTML","inputs","length","requestSubmit","remove"],"mappings":"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"}
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":"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"}
@@ -20,27 +20,24 @@ module Katalyst
20
20
  private
21
21
 
22
22
  def row_proc
23
- @row_proc ||= @__vc_render_in_block || method(:row_partial)
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
31
+ end
24
32
  end
25
33
 
26
- def row_partial(row, record = nil)
27
- @partial ||= partial_path
28
- @as ||= template_name
29
- render(partial: @partial, variants: [:row], formats: [:html], locals: { @as => record, row: row })
30
- end
31
-
32
- def partial_path
33
- # Collection::Base overwrites param_key for form_with compatibility
34
- items.model_name.param_key.to_s
35
- end
36
-
37
- def template_name
38
- # Collection::Base overwrites param_key for form_with compatibility
39
- items.model_name.param_key.to_sym
40
- end
41
-
42
- def items
43
- collection.respond_to?(:items) ? collection.items : collection
34
+ def row_renderer
35
+ @row_renderer ||= RowRenderer.new(@lookup_context,
36
+ collection: collection,
37
+ as: @as,
38
+ partial: @partial,
39
+ variants: [:row],
40
+ formats: [:html])
44
41
  end
45
42
  end
46
43
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ class RowRenderer < ActionView::PartialRenderer # :nodoc:
6
+ include ObjectRendering
7
+
8
+ def initialize(lookup_context, collection:, partial:, **options)
9
+ super(lookup_context, options)
10
+
11
+ @collection = collection
12
+ @partial = partial
13
+ end
14
+
15
+ def render_row(row, object, view_context)
16
+ @row = row
17
+ @object = object
18
+
19
+ if @partial.blank?
20
+ example = example_for(@collection)
21
+ @partial = partial_path(example, view_context) if example.present?
22
+ end
23
+
24
+ # if we still cannot find an example return an empty table (no header row)
25
+ return "" if @partial.blank?
26
+
27
+ @local_name ||= local_variable(@partial)
28
+ render(@partial, view_context, nil)
29
+ end
30
+
31
+ private
32
+
33
+ def example_for(collection)
34
+ if collection.respond_to?(:items)
35
+ example_for(collection.items)
36
+ elsif collection.respond_to?(:any?) && collection.any?
37
+ collection.first
38
+ elsif collection.respond_to?(:model)
39
+ collection.model.new
40
+ end
41
+ # if none of the above strategies match, return nil
42
+ rescue ArgumentError
43
+ nil # if we could not construct an example without passing arguments, return nil
44
+ end
45
+
46
+ def template_keys(path)
47
+ super + [@local_name, :row]
48
+ end
49
+
50
+ def render_partial_template(view, locals, template, layout, block)
51
+ locals[@local_name || template.variable] = @object
52
+ locals[:row] = @row
53
+ super(view, locals, template, layout, block)
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ # Adds checkbox selection to a table.
6
+ # See [documentation](/docs/selectable.md) for more details.
7
+ module Selectable
8
+ extend ActiveSupport::Concern
9
+
10
+ FORM_CONTROLLER = "tables--selection--form"
11
+ ITEM_CONTROLLER = "tables--selection--item"
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
70
+ end
71
+
72
+ module BodyRow # :nodoc:
73
+ def selection
74
+ id = @record.public_send(@table.selection.primary_key)
75
+ params = {
76
+ id: 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
+ }) do
86
+ tag.input(type: :checkbox)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -30,7 +30,7 @@ module Katalyst
30
30
  # - `header`: whether to render the header row (defaults to true, supports options)
31
31
  # - `caption`: whether to render the caption (defaults to true, supports options)
32
32
  # - `object_name`: the name of the object to use for partial rendering (defaults to collection.model_name.i18n_key)
33
- # - `partial`: the name of the partial to use for rendering each row (defaults to collection.model_name.param_key)
33
+ # - `partial`: the name of the partial to use for rendering each row (defaults to to_partial_path on the object)
34
34
  # - `as`: the name of the local variable to use for rendering each row (defaults to collection.model_name.param_key)
35
35
  # In addition to these options, standard HTML attributes can be passed which will be added to the table tag.
36
36
  def initialize(collection:,
@@ -0,0 +1,13 @@
1
+ <%= form_with(method: :patch,
2
+ id: id,
3
+ class: "tables--selection--form",
4
+ data: { controller: form_controller },
5
+ html: { hidden: "" }) do |form| %>
6
+ <p class="tables--selection--summary">
7
+ <span data-<%= form_target("count") %>>0</span>
8
+ <span data-<%= form_target("singular") %> hidden><%= @table.collection.model_name.singular %></span>
9
+ <span data-<%= form_target("plural") %>><%= @table.collection.model_name.plural %></span>
10
+ selected
11
+ </p>
12
+ <%= content %>
13
+ <% end %>
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Selectable
6
+ class FormComponent < ViewComponent::Base # :nodoc:
7
+ attr_reader :id, :primary_key
8
+
9
+ def initialize(table:,
10
+ id: nil,
11
+ primary_key: :id)
12
+ super
13
+
14
+ @table = table
15
+ @id = id
16
+ @primary_key = primary_key
17
+
18
+ if @id.nil?
19
+ table_id = table.try(:id)
20
+
21
+ raise ArgumentError, "Table selection requires an id" if table_id.nil?
22
+
23
+ @id = "#{table_id}_selection"
24
+ end
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.name} id: #{id.inspect}, primary_key: #{primary_key.inspect}>"
29
+ end
30
+
31
+ private
32
+
33
+ def form_controller
34
+ FORM_CONTROLLER
35
+ end
36
+
37
+ def form_target(value)
38
+ "#{FORM_CONTROLLER}-target=#{value}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,7 +1,9 @@
1
1
  import TurboCollectionController from "./turbo/collection_controller";
2
- import ItemController from "./orderable/item_controller";
3
- import ListController from "./orderable/list_controller";
4
- import FormController from "./orderable/form_controller";
2
+ import OrderableItemController from "./orderable/item_controller";
3
+ import OrderableListController from "./orderable/list_controller";
4
+ import OrderableFormController from "./orderable/form_controller";
5
+ import SelectionFormController from "./selection/form_controller";
6
+ import SelectionItemController from "./selection/item_controller";
5
7
 
6
8
  const Definitions = [
7
9
  {
@@ -10,15 +12,23 @@ const Definitions = [
10
12
  },
11
13
  {
12
14
  identifier: "tables--orderable--item",
13
- controllerConstructor: ItemController,
15
+ controllerConstructor: OrderableItemController,
14
16
  },
15
17
  {
16
18
  identifier: "tables--orderable--list",
17
- controllerConstructor: ListController,
19
+ controllerConstructor: OrderableListController,
18
20
  },
19
21
  {
20
22
  identifier: "tables--orderable--form",
21
- controllerConstructor: FormController,
23
+ controllerConstructor: OrderableFormController,
24
+ },
25
+ {
26
+ identifier: "tables--selection--form",
27
+ controllerConstructor: SelectionFormController,
28
+ },
29
+ {
30
+ identifier: "tables--selection--item",
31
+ controllerConstructor: SelectionItemController,
22
32
  },
23
33
  ];
24
34
 
@@ -0,0 +1,60 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class SelectionFormController extends Controller {
4
+ static values = {
5
+ count: Number,
6
+ primaryKey: { type: String, default: "id" },
7
+ };
8
+ static targets = ["count", "singular", "plural"];
9
+
10
+ connect() {
11
+ this.countValue = this.inputs.length;
12
+ }
13
+
14
+ /**
15
+ * @param id to toggle
16
+ * @return {boolean} true if selected, false if unselected
17
+ */
18
+ toggle(id) {
19
+ const input = this.input(id);
20
+
21
+ if (input) {
22
+ input.remove();
23
+ } else {
24
+ this.element.insertAdjacentHTML(
25
+ "beforeend",
26
+ `<input type="hidden" name="${this.primaryKeyValue}[]" value="${id}">`,
27
+ );
28
+ }
29
+
30
+ this.countValue = this.inputs.length;
31
+
32
+ return !input;
33
+ }
34
+
35
+ /**
36
+ * @returns {boolean} true if the given id is currently selected
37
+ */
38
+ isSelected(id) {
39
+ return !!this.input(id);
40
+ }
41
+
42
+ get inputs() {
43
+ return this.element.querySelectorAll(
44
+ `input[name="${this.primaryKeyValue}[]"]`,
45
+ );
46
+ }
47
+
48
+ input(id) {
49
+ return this.element.querySelector(
50
+ `input[name="${this.primaryKeyValue}[]"][value="${id}"]`,
51
+ );
52
+ }
53
+
54
+ countValueChanged(count) {
55
+ this.element.toggleAttribute("hidden", count === 0);
56
+ this.countTarget.textContent = count;
57
+ this.singularTarget.toggleAttribute("hidden", count !== 1);
58
+ this.pluralTarget.toggleAttribute("hidden", count === 1);
59
+ }
60
+ }
@@ -0,0 +1,27 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class SelectionItemController extends Controller {
4
+ static outlets = ["tables--selection--form"];
5
+ static values = {
6
+ params: Object,
7
+ checked: Boolean,
8
+ };
9
+
10
+ tablesSelectionFormOutletConnected(form) {
11
+ this.checkedValue = form.isSelected(this.id);
12
+ }
13
+
14
+ change(e) {
15
+ e.preventDefault();
16
+
17
+ this.checkedValue = this.tablesSelectionFormOutlet.toggle(this.id);
18
+ }
19
+
20
+ get id() {
21
+ return this.paramsValue.id;
22
+ }
23
+
24
+ checkedValueChanged(checked) {
25
+ this.element.querySelector("input").checked = checked;
26
+ }
27
+ }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-tables
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.6.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-08 00:00:00.000000000 Z
11
+ date: 2024-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: katalyst-html-attributes
@@ -57,6 +57,8 @@ files:
57
57
  - app/components/concerns/katalyst/tables/configurable_component.rb
58
58
  - app/components/concerns/katalyst/tables/has_table_content.rb
59
59
  - app/components/concerns/katalyst/tables/orderable.rb
60
+ - app/components/concerns/katalyst/tables/row_renderer.rb
61
+ - app/components/concerns/katalyst/tables/selectable.rb
60
62
  - app/components/concerns/katalyst/tables/sortable.rb
61
63
  - app/components/concerns/katalyst/tables/turbo_replaceable.rb
62
64
  - app/components/katalyst/table_component.html.erb
@@ -68,6 +70,8 @@ files:
68
70
  - app/components/katalyst/tables/header_cell_component.rb
69
71
  - app/components/katalyst/tables/header_row_component.rb
70
72
  - app/components/katalyst/tables/pagy_nav_component.rb
73
+ - app/components/katalyst/tables/selectable/form_component.html.erb
74
+ - app/components/katalyst/tables/selectable/form_component.rb
71
75
  - app/components/katalyst/turbo/pagy_nav_component.rb
72
76
  - app/components/katalyst/turbo/table_component.rb
73
77
  - app/controllers/concerns/katalyst/tables/backend.rb
@@ -77,6 +81,8 @@ files:
77
81
  - app/javascript/tables/orderable/form_controller.js
78
82
  - app/javascript/tables/orderable/item_controller.js
79
83
  - app/javascript/tables/orderable/list_controller.js
84
+ - app/javascript/tables/selection/form_controller.js
85
+ - app/javascript/tables/selection/item_controller.js
80
86
  - app/javascript/tables/turbo/collection_controller.js
81
87
  - app/models/concerns/katalyst/tables/collection/core.rb
82
88
  - app/models/concerns/katalyst/tables/collection/filtering.rb
@@ -111,11 +117,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
111
117
  version: 3.0.0
112
118
  required_rubygems_version: !ruby/object:Gem::Requirement
113
119
  requirements:
114
- - - ">="
120
+ - - ">"
115
121
  - !ruby/object:Gem::Version
116
- version: '0'
122
+ version: 1.3.1
117
123
  requirements: []
118
- rubygems_version: 3.4.10
124
+ rubygems_version: 3.4.19
119
125
  signing_key:
120
126
  specification_version: 4
121
127
  summary: HTML table generator for Rails views