katalyst-tables 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +2 -0
- data/app/assets/builds/katalyst/tables.esm.js +16 -0
- data/app/assets/builds/katalyst/tables.js +16 -0
- data/app/assets/builds/katalyst/tables.min.js +1 -1
- data/app/assets/builds/katalyst/tables.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/tables/_filter.scss +34 -0
- data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -0
- data/app/assets/stylesheets/katalyst/tables/_select.scss +3 -0
- data/app/assets/stylesheets/katalyst/tables/_table.scss +3 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_enum.scss +9 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss +1 -0
- data/app/components/katalyst/table_component.rb +28 -0
- data/app/components/katalyst/tables/cells/enum_component.rb +27 -0
- data/app/components/katalyst/tables/filter/modal_component.html.erb +25 -0
- data/app/components/katalyst/tables/filter/modal_component.rb +66 -0
- data/app/components/katalyst/tables/filter_component.html.erb +20 -0
- data/app/components/katalyst/tables/filter_component.rb +91 -0
- data/app/helpers/katalyst/tables/frontend.rb +8 -0
- data/app/javascript/tables/application.js +5 -0
- data/app/javascript/tables/filter/modal_controller.js +13 -0
- data/app/models/concerns/katalyst/tables/collection/core.rb +30 -0
- data/app/models/concerns/katalyst/tables/collection/filtering.rb +80 -9
- data/app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb +56 -0
- data/app/models/concerns/katalyst/tables/collection/query/parser.rb +65 -0
- data/app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb +24 -0
- data/app/models/concerns/katalyst/tables/collection/query/value_parser.rb +34 -0
- data/app/models/concerns/katalyst/tables/collection/query.rb +43 -0
- data/app/models/katalyst/tables/collection/array.rb +0 -1
- data/app/models/katalyst/tables/collection/base.rb +0 -5
- data/app/models/katalyst/tables/collection/filter.rb +0 -1
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc8fcc04168c193e4efc390aea35902880a43634861792f099a6307deee8b1ce
|
4
|
+
data.tar.gz: 87a482c6703813d83cc99a71f54fc5eb23ec3fdb71d3aebd21ad0cf83afde7ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b48963d7042ca31ee030a5f8b822ec0a2869a80d5f488baff838d113c238df1629576e3c12b9927c34c51f0e2ff0d556b8335fd2525e61cc0339130e0ea22b8e
|
7
|
+
data.tar.gz: 6c88896aa14bdb09dc8495b730812909b2b97ce3b6819409032398e0dcb1f475de709b3394584f3a501ed88847a84dca4054232a5b8ef401e3a5560060ca3f04
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -221,6 +221,8 @@ You can use the `Katalyst::SummaryTableComponent` to render a single record util
|
|
221
221
|
|
222
222
|
The following extensions are available and activated by default:
|
223
223
|
|
224
|
+
* [Filtering](docs/filtering.md) - adds automatic collection filtering based on attributes
|
225
|
+
* [Query](docs/query.md) - adds human-friendly text filtering that populates collection attributes
|
224
226
|
* [Identifiable](docs/identifiable.md) - adds default dom ids to the table and data rows.
|
225
227
|
* [Orderable](docs/orderable.md) - adds bulk-update for 'ordinal' columns via dragging rows in the table.
|
226
228
|
* [Pagination](docs/pagination.md) - handles paginating of data in the collection.
|
@@ -533,6 +533,18 @@ class SelectionItemController extends Controller {
|
|
533
533
|
}
|
534
534
|
}
|
535
535
|
|
536
|
+
class FilterModalController extends Controller {
|
537
|
+
static targets = ["modal"];
|
538
|
+
|
539
|
+
close(e) {
|
540
|
+
delete this.modalTarget.dataset.open;
|
541
|
+
}
|
542
|
+
|
543
|
+
open(e) {
|
544
|
+
this.modalTarget.dataset.open = "true";
|
545
|
+
}
|
546
|
+
}
|
547
|
+
|
536
548
|
const Definitions = [
|
537
549
|
{
|
538
550
|
identifier: "tables--orderable--item",
|
@@ -554,6 +566,10 @@ const Definitions = [
|
|
554
566
|
identifier: "tables--selection--item",
|
555
567
|
controllerConstructor: SelectionItemController,
|
556
568
|
},
|
569
|
+
{
|
570
|
+
identifier: "tables--filter--modal",
|
571
|
+
controllerConstructor: FilterModalController,
|
572
|
+
},
|
557
573
|
];
|
558
574
|
|
559
575
|
export { Definitions as default };
|
@@ -533,6 +533,18 @@ class SelectionItemController extends Controller {
|
|
533
533
|
}
|
534
534
|
}
|
535
535
|
|
536
|
+
class FilterModalController extends Controller {
|
537
|
+
static targets = ["modal"];
|
538
|
+
|
539
|
+
close(e) {
|
540
|
+
delete this.modalTarget.dataset.open;
|
541
|
+
}
|
542
|
+
|
543
|
+
open(e) {
|
544
|
+
this.modalTarget.dataset.open = "true";
|
545
|
+
}
|
546
|
+
}
|
547
|
+
|
536
548
|
const Definitions = [
|
537
549
|
{
|
538
550
|
identifier: "tables--orderable--item",
|
@@ -554,6 +566,10 @@ const Definitions = [
|
|
554
566
|
identifier: "tables--selection--item",
|
555
567
|
controllerConstructor: SelectionItemController,
|
556
568
|
},
|
569
|
+
{
|
570
|
+
identifier: "tables--filter--modal",
|
571
|
+
controllerConstructor: FilterModalController,
|
572
|
+
},
|
557
573
|
];
|
558
574
|
|
559
575
|
export { Definitions as default };
|
@@ -1,2 +1,2 @@
|
|
1
|
-
import{Controller as t}from"@hotwired/stimulus";class e{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 s 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 i=[{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}params(t){const{id_name:e,id_value:s,index_name:i}=this.paramsValue;return[{name:`${t}[${s}][${e}]`,value:this.id},{name:`${t}[${s}][${i}]`,value:this.index}]}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 s=this.#e(t.target);s&&(t.preventDefault(),this.startDragging(new e(this.element,t,s.id)),this.dragState.updateCursor(this.element,s.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.#s.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#s(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#e(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{static values={scope:String};add(t){t.params(this.scopeValue).forEach((({name:t,value:e})=>{this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${t}" value="${e}" 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:s}];export{i as default};
|
1
|
+
import{Controller as t}from"@hotwired/stimulus";class e{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 s 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 i=[{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}params(t){const{id_name:e,id_value:s,index_name:i}=this.paramsValue;return[{name:`${t}[${s}][${e}]`,value:this.id},{name:`${t}[${s}][${i}]`,value:this.index}]}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 s=this.#e(t.target);s&&(t.preventDefault(),this.startDragging(new e(this.element,t,s.id)),this.dragState.updateCursor(this.element,s.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.#s.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#s(){return this.tablesOrderableItemOutlets.toSorted(((t,e)=>t.comparisonIndex-e.comparisonIndex))}#e(t){return this.tablesOrderableItemOutlets.find((e=>e.element===t))}}},{identifier:"tables--orderable--form",controllerConstructor:class extends t{static values={scope:String};add(t){t.params(this.scopeValue).forEach((({name:t,value:e})=>{this.element.insertAdjacentHTML("beforeend",`<input type="hidden" name="${t}" value="${e}" 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:s},{identifier:"tables--filter--modal",controllerConstructor:class extends t{static targets=["modal"];close(t){delete this.modalTarget.dataset.open}open(t){this.modalTarget.dataset.open="true"}}}];export{i 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/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":"gDA8NA,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,ECnBE,MAACe,EAAc,CAClB,CACEC,WAAY,0BACZC,sBCPW,cAAqCrB,EAClDC,cAAgB,CACdC,OAAQC,QAGV,OAAAmB,GA6GF,IAAkBL,EAzGd1C,KAAKgD,OAyGSN,EAzGQ1C,KAAKQ,IA0GtByC,MAAMC,KAAKR,EAAQS,cAAcC,UAAUC,QAAQX,GAzGzD,CAED,kBAAAY,CAAmB3B,GACjB3B,KAAKD,GAAK4B,EAAO4B,QAClB,CAED,UAAAC,CAAWC,GACTzD,KAAK0D,WAAaD,EAClBzD,KAAKQ,IAAImD,MAAMxC,SAAW,WAC1BnB,KAAKQ,IAAImD,MAAM/C,IAAM6C,EAAS,KAC9BzD,KAAKQ,IAAImD,MAAMC,OAAS,IACxB5D,KAAKQ,IAAIqD,gBAAgB,YAAY,EACtC,CAQD,cAAAC,CAAed,GACbhD,KAAKQ,IAAImD,MAAMxC,SAAW,WAC1BnB,KAAKQ,IAAImD,MAAM/C,IACbZ,KAAKQ,IAAIe,cAAgByB,EAAQhD,KAAK+D,WADnB,IAGtB,CASD,WAAAC,CAAYhB,GACVhD,KAAKgD,MAAQA,CACd,CAGD,MAAArB,CAAOsC,GACL,MAAMC,QAAEA,EAAOX,SAAEA,EAAQY,WAAEA,GAAenE,KAAKwC,YAC/C,MAAO,CACL,CAAE4B,KAAM,GAAGH,KAASV,MAAaW,KAAYG,MAAOrE,KAAKD,IACzD,CAAEqE,KAAM,GAAGH,KAASV,MAAaY,KAAeE,MAAOrE,KAAKgD,OAE/D,CAKD,KAAAsB,UACStE,KAAK0D,WACZ1D,KAAKQ,IAAI+D,gBAAgB,SACzBvE,KAAKQ,IAAI+D,gBAAgB,WAC1B,CAKD,cAAIC,GACF,OAAOxE,KAAKwC,YAAYiC,cAAgBzE,KAAKgD,KAC9C,CASD,aAAIe,GACF,OAAI/D,KAAK0D,YAAkC,IAApB1D,KAAK0D,WACnB1D,KAAKgD,MAAQ5B,KAAKsD,MAAM1E,KAAK0D,WAAa1D,KAAKQ,IAAIe,cAEnDvB,KAAKgD,KAEf,CAUD,mBAAI2B,GACF,OAAI3E,KAAK0D,WACA1D,KAAK+D,WAAa/D,KAAK0D,WAAa,EAAI,IAAO,IAE/C1D,KAAKgD,KAEf,CAOD,OAAIxC,GACF,OAAOR,KAAK0C,QAAQS,aACrB,IDtGD,CACEN,WAAY,0BACZC,sBFXW,cAAsCrB,EACnDC,eAAiB,CAAC,0BAA2B,2BAI7C,aAAAkD,CAAcC,GACZ7E,KAAK6E,UAAYA,EAEjBC,SAASC,iBAAiB,YAAa/E,KAAKgF,WAC5CF,SAASC,iBAAiB,UAAW/E,KAAKiF,SAC1CC,OAAOH,iBAAiB,SAAU/E,KAAKmF,QAAQ,GAE/CnF,KAAK0C,QAAQiB,MAAMxC,SAAW,UAC/B,CAED,YAAAiE,GACE,MAAMP,EAAY7E,KAAK6E,UAUvB,cATO7E,KAAK6E,UAEZC,SAASO,oBAAoB,YAAarF,KAAKgF,WAC/CF,SAASO,oBAAoB,UAAWrF,KAAKiF,SAC7CC,OAAOG,oBAAoB,SAAUrF,KAAKmF,QAAQ,GAElDnF,KAAK0C,QAAQ6B,gBAAgB,SAC7BvE,KAAKsF,2BAA2BC,SAASC,GAASA,EAAKlB,UAEhDO,CACR,CAED,IAAAY,GAKE,MAAMC,EAAW1F,KAAK0F,SAEtB,IAAKA,EAAU,OAEf,MAAMC,EAAWD,EAAS3B,UACpB6B,EAAa5F,KAAKsF,2BAA2BK,GAE9CC,IAGDD,EAAWD,EAAS1C,MACtB4C,EAAWpF,IAAIqF,sBAAsB,cAAeH,EAASlF,KACpDmF,EAAWD,EAAS1C,OAC7B4C,EAAWpF,IAAIqF,sBAAsB,WAAYH,EAASlF,KAI5DR,KAAKsF,2BAA2BC,SAAQ,CAACC,EAAMxC,IAC7CwC,EAAKxB,YAAYhB,KAInBhD,KAAK8F,gBACN,CAED,aAAAA,GAEE9F,KAAK+F,0BAA0BC,QAG/BhG,KAAKsF,2BAA2BC,SAASC,IACnCA,EAAKhB,YAAYxE,KAAK+F,0BAA0BE,IAAIT,EAAK,IAG/DxF,KAAK+F,0BAA0BG,QAChC,CAMD,SAAAC,CAAUrG,GACR,GAAIE,KAAKoG,WAAY,OAErB,MAAMhG,EAASJ,MAAK4F,EAAY9F,EAAMM,QAEjCA,IAELN,EAAMuC,iBAENrC,KAAK4E,cAAc,IAAIjF,EAAUK,KAAK0C,QAAS5C,EAAOM,EAAOL,KAE7DC,KAAK6E,UAAUtE,aAAaP,KAAK0C,QAAStC,EAAOI,IAAKV,EAAOE,KAAKqG,SACnE,CAEDrB,UAAalF,IACNE,KAAKoG,aAEVtG,EAAMuC,iBAEFrC,KAAKsG,UAETtG,KAAKsG,SAAU,EAEfpB,OAAOqB,uBAAsB,KAC3BvG,KAAKsG,SAAU,EACftG,KAAK6E,UAAUtE,aACbP,KAAK0C,QACL1C,KAAK0F,SAASlF,IACdV,EACAE,KAAKqG,QACN,KACD,EAGJlB,OAAUrF,IACHE,KAAKoG,aAAcpG,KAAKsG,UAE7BtG,KAAKsG,SAAU,EAEfpB,OAAOqB,uBAAsB,KAC3BvG,KAAKsG,SAAU,EACftG,KAAK6E,UAAU7D,aACbhB,KAAK0C,QACL1C,KAAK0F,SAASlF,IACdR,KAAKqG,QACN,IACD,EAGJpB,QAAWnF,IACJE,KAAKoG,aAEVpG,KAAKyF,OACLzF,KAAKoF,eACLpF,KAAKwG,2BAA2BjB,SAASvD,UAAgBA,EAAK6C,YAAU,EAG1E,kCAAA4B,CAAmCzE,EAAMU,GACnCV,EAAK6C,WAEP7E,KAAK4E,cAAc5C,EAAK6C,UAE3B,CAED,qCAAA6B,CAAsC1E,EAAMU,GACtC1C,KAAKoG,aAEPpE,EAAK6C,UAAY7E,KAAKoF,eAEzB,CAaDiB,QAAW5C,IACT,MAAMiC,EAAW1F,KAAK0F,SAGtBA,EAASlC,WAAWC,GAIpBzD,MAAK2G,EAAcpB,SAAQ,CAACC,EAAMxC,KAC5BwC,IAASE,GACbF,EAAK1B,eAAed,EAAM,GAC1B,EAGJ,cAAIoD,GACF,QAASpG,KAAK6E,SACf,CAED,YAAIa,GACF,OAAK1F,KAAKoG,WAEHpG,KAAKsF,2BAA2BsB,MACpCpB,GAASA,EAAKzF,KAAOC,KAAK6E,UAAUvE,WAHV,IAK9B,CAQD,KAAIqG,GACF,OAAO3G,KAAKsF,2BAA2BuB,UACrC,CAACC,EAAGC,IAAMD,EAAEnC,gBAAkBoC,EAAEpC,iBAEnC,CAQD,EAAAiB,CAAYlD,GACV,OAAO1C,KAAKsF,2BAA2BsB,MACpCpB,GAASA,EAAK9C,UAAYA,GAE9B,IEhMD,CACEG,WAAY,0BACZC,sBEfW,cAAsCrB,EACnDC,cAAgB,CAAEuC,MAAO+C,QAEzB,GAAAf,CAAIT,GACFA,EAAK7D,OAAO3B,KAAKiH,YAAY1B,SAAQ,EAAGnB,OAAMC,YAC5CrE,KAAK0C,QAAQwE,mBACX,YACA,8BAA8B9C,aAAgBC,qBAC/C,GAEJ,CAED,MAAA6B,GAC6B,IAAvBlG,KAAKmH,OAAOC,QAEhBpH,KAAK0C,QAAQ2E,eACd,CAED,KAAArB,GACEhG,KAAKmH,OAAO5B,SAAS+B,GAAUA,EAAMC,UACtC,CAED,UAAIJ,GACF,OAAOnH,KAAK0C,QAAQ8E,iBAAiB,wBACtC,IFPD,CACE3E,WAAY,0BACZC,sBGnBW,cAAsCrB,EACnDC,cAAgB,CACd+F,MAAOC,OACPC,WAAY,CAAEC,KAAMZ,OAAQa,QAAS,OAEvCnG,eAAiB,CAAC,QAAS,WAAY,UAEvC,OAAAqB,GACE/C,KAAK8H,WAAa9H,KAAKmH,OAAOC,MAC/B,CAMD,MAAA7E,CAAOxC,GACL,MAAMuH,EAAQtH,KAAKsH,MAAMvH,GAazB,OAXIuH,EACFA,EAAMC,SAENvH,KAAK0C,QAAQwE,mBACX,YACA,8BAA8BlH,KAAK+H,6BAA6BhI,OAIpEC,KAAK8H,WAAa9H,KAAKmH,OAAOC,QAEtBE,CACT,CAKD,UAAApF,CAAWnC,GACT,QAASC,KAAKsH,MAAMvH,EACrB,CAED,UAAIoH,GACF,OAAOnH,KAAK0C,QAAQ8E,iBAClB,eAAexH,KAAK+H,sBAEvB,CAED,KAAAT,CAAMvH,GACJ,OAAOC,KAAK0C,QAAQC,cAClB,eAAe3C,KAAK+H,8BAA8BhI,MAErD,CAED,iBAAAiI,CAAkBP,GAChBzH,KAAK0C,QAAQmB,gBAAgB,SAAoB,IAAV4D,GACvCzH,KAAKiI,YAAYC,YAAcT,EAC/BzH,KAAKmI,eAAetE,gBAAgB,SAAoB,IAAV4D,GAC9CzH,KAAKoI,aAAavE,gBAAgB,SAAoB,IAAV4D,EAC7C,IHnCD,CACE5E,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","../../../javascript/tables/filter/modal_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\";\nimport FilterModalController from \"./filter/modal_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 identifier: \"tables--filter--modal\",\n controllerConstructor: FilterModalController,\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","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class FilterModalController extends Controller {\n static targets = [\"modal\"];\n\n close(e) {\n delete this.modalTarget.dataset.open;\n }\n\n open(e) {\n this.modalTarget.dataset.open = \"true\";\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","close","modalTarget","dataset","open"],"mappings":"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"}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
[data-controller="tables--filter--modal"] {
|
2
|
+
position: relative;
|
3
|
+
}
|
4
|
+
|
5
|
+
.filter-keys-modal {
|
6
|
+
position: absolute;
|
7
|
+
top: 100%;
|
8
|
+
left: 0;
|
9
|
+
right: 0;
|
10
|
+
border: 1px solid rgba(0, 0, 0, 0.16);
|
11
|
+
box-shadow:
|
12
|
+
0 3px 6px rgba(0, 0, 0, 0.16),
|
13
|
+
0 3px 6px rgba(0, 0, 0, 0.23);
|
14
|
+
padding-inline: 1rem;
|
15
|
+
padding-block: 0.5rem 0;
|
16
|
+
margin-top: 0.5rem;
|
17
|
+
background: white;
|
18
|
+
border-radius: 4px;
|
19
|
+
z-index: 1;
|
20
|
+
opacity: 0;
|
21
|
+
transition: opacity 0.125s;
|
22
|
+
pointer-events: none;
|
23
|
+
|
24
|
+
&[data-open] {
|
25
|
+
opacity: 1;
|
26
|
+
pointer-events: unset;
|
27
|
+
}
|
28
|
+
|
29
|
+
.footer {
|
30
|
+
display: flex;
|
31
|
+
justify-content: flex-end;
|
32
|
+
padding-block: 1rem;
|
33
|
+
}
|
34
|
+
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
@use "ordinal" as *;
|
2
|
+
@use "select" as *;
|
2
3
|
@use "typed-columns";
|
3
4
|
|
4
5
|
$grey: #f0ecf3 !default;
|
@@ -7,6 +8,7 @@ $table-header-color: transparent !default;
|
|
7
8
|
$row-border-color: $grey !default;
|
8
9
|
$row-height: 48px !default;
|
9
10
|
$cell-spacing: 0.5rem !default;
|
11
|
+
$tag-color: $grey !default;
|
10
12
|
|
11
13
|
$width-small: 6rem !default;
|
12
14
|
$width-medium: 12rem !default;
|
@@ -17,6 +19,7 @@ table {
|
|
17
19
|
--cell-spacing: #{$cell-spacing};
|
18
20
|
--table-header-color: #{$table-header-color};
|
19
21
|
--row-border-color: #{$row-border-color};
|
22
|
+
--tag-color: #{$tag-color};
|
20
23
|
|
21
24
|
--width-small: #{$width-small};
|
22
25
|
--width-medium: #{$width-medium};
|
@@ -209,6 +209,34 @@ module Katalyst
|
|
209
209
|
), &)
|
210
210
|
end
|
211
211
|
|
212
|
+
# Generates a column from an enum value rendered as a tag.
|
213
|
+
# The target attribute must be defined as an `enum` in the model.
|
214
|
+
#
|
215
|
+
# @param column [Symbol] the column's name, called as a method on the record.
|
216
|
+
# @param label [String|nil] the label to use for the column header
|
217
|
+
# @param heading [boolean] if true, data cells will use `th` tags
|
218
|
+
# @param ** [Hash] HTML attributes to be added to column cells
|
219
|
+
# @param & [Proc] optional block to wrap the cell content
|
220
|
+
#
|
221
|
+
# When rendering an enum value, the component will check for translations
|
222
|
+
# using the key `active_record.attributes.[model]/[column].[value]`,
|
223
|
+
# e.g. `active_record.attributes.banner/status.published`.
|
224
|
+
#
|
225
|
+
# If a block is provided, it will be called with the cell component as an argument.
|
226
|
+
# @yieldparam cell [Katalyst::Tables::CellComponent] the cell component
|
227
|
+
#
|
228
|
+
# @return [void]
|
229
|
+
#
|
230
|
+
# @example Render a generic text column for any value that supports `to_s`
|
231
|
+
# <% row.enum :status %>
|
232
|
+
# <%# label => <th>Status</th> %>
|
233
|
+
# <%# data => <td class="type-enum"><span data-enum="status" data-value="published">Published</span></td> %>
|
234
|
+
def enum(column, label: nil, heading: false, **, &)
|
235
|
+
with_cell(Tables::Cells::EnumComponent.new(
|
236
|
+
collection:, row:, column:, record:, label:, heading:, **,
|
237
|
+
), &)
|
238
|
+
end
|
239
|
+
|
212
240
|
# Generates a column from numeric values formatted appropriately.
|
213
241
|
#
|
214
242
|
# @param column [Symbol] the column's name, called as a method on the record
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Cells
|
6
|
+
# Displays an enum value using data inferred from the model.
|
7
|
+
class EnumComponent < CellComponent
|
8
|
+
def rendered_value
|
9
|
+
if (value = self.value).present?
|
10
|
+
label = t(i18n_enum_label_key(value), default: value)
|
11
|
+
content_tag(:small, label, data: { enum: column, value: })
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def default_html_attributes
|
18
|
+
{ class: "type-enum" }
|
19
|
+
end
|
20
|
+
|
21
|
+
def i18n_enum_label_key(value)
|
22
|
+
"active_record.attributes.#{collection.model_name.i18n_key}/#{column}.#{value}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<%= tag.div(**html_attributes) do %>
|
2
|
+
<table>
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th></th>
|
6
|
+
<th>Key</th>
|
7
|
+
<th>Values</th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<% attributes.each do |key, attribute| %>
|
12
|
+
<tr>
|
13
|
+
<th><%= collection.model.human_attribute_name(key) %></th>
|
14
|
+
<td><%= key %></td>
|
15
|
+
<td><%= values_for(key, attribute) %></td>
|
16
|
+
</tr>
|
17
|
+
<% end %>
|
18
|
+
</tbody>
|
19
|
+
</table>
|
20
|
+
<% if footer? %>
|
21
|
+
<div class="footer">
|
22
|
+
<%= footer %>
|
23
|
+
</div>
|
24
|
+
<% end %>
|
25
|
+
<% end %>
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Filter
|
6
|
+
class ModalComponent < ViewComponent::Base
|
7
|
+
include Katalyst::HtmlAttributes
|
8
|
+
include Katalyst::Tables::Frontend
|
9
|
+
|
10
|
+
DEFAULT_ATTRIBUTES = %w[page sort search query].freeze
|
11
|
+
|
12
|
+
renders_one :footer
|
13
|
+
|
14
|
+
attr_reader :collection, :url
|
15
|
+
|
16
|
+
def initialize(collection:, **)
|
17
|
+
super(**)
|
18
|
+
|
19
|
+
@collection = collection
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_html_attributes
|
25
|
+
{
|
26
|
+
class: "filter-keys-modal",
|
27
|
+
data: {
|
28
|
+
tables__filter__modal_target: "modal",
|
29
|
+
},
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes
|
34
|
+
collection.class.attribute_types.except(*DEFAULT_ATTRIBUTES)
|
35
|
+
end
|
36
|
+
|
37
|
+
def values_for(key, attribute)
|
38
|
+
values_method = "#{key.parameterize.underscore}_values"
|
39
|
+
if attribute.type == :boolean
|
40
|
+
render_options(true, false)
|
41
|
+
elsif collection.model.defined_enums.has_key?(key)
|
42
|
+
render_array(*collection.model.defined_enums[key].keys)
|
43
|
+
elsif collection.respond_to?(values_method)
|
44
|
+
if collection.class.enum_attribute?(key)
|
45
|
+
render_array(*collection.public_send(values_method))
|
46
|
+
else
|
47
|
+
render_options(*collection.public_send(values_method))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_option(value)
|
53
|
+
"<code>#{value}</code>".html_safe # rubocop:disable Rails/OutputSafety
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_options(*values)
|
57
|
+
safe_join(values.map { |value| render_option(value) }, ", ")
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_array(*values)
|
61
|
+
safe_join(["[", render_options(*values), "]"])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%= tag.div(**html_attributes) do %>
|
2
|
+
<% if content? %>
|
3
|
+
<%= content %>
|
4
|
+
<% else %>
|
5
|
+
<%= form do |form| %>
|
6
|
+
<%= form.text_field(
|
7
|
+
:query,
|
8
|
+
type: :search,
|
9
|
+
size: :full,
|
10
|
+
label: nil,
|
11
|
+
autocomplete: "off",
|
12
|
+
**input_attributes,
|
13
|
+
) %>
|
14
|
+
|
15
|
+
<%= form.submit("Apply") %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<%= modal %>
|
20
|
+
<% end %>
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
# A component for rendering a data driven filter for a collection.
|
6
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) %>
|
7
|
+
#
|
8
|
+
# By default, the component will render a form containing a single text field. Interacting with the
|
9
|
+
# text field will display a dropdown outlining all available keys and values to be filtered on.
|
10
|
+
#
|
11
|
+
# You can override how the form and input displays by passing in content to the component.
|
12
|
+
# The component provides a helper function `form`
|
13
|
+
# to ensure the correct attributes and default form fields are collected.
|
14
|
+
# You can pass additional options to the `form` method to modify it.
|
15
|
+
#
|
16
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
|
17
|
+
# <%= filter.form(builder: GOVUKFormBuilder) do |form| %>
|
18
|
+
# <%= form.govuk_text_field :query %>
|
19
|
+
# <%= form.govuk_submit "Apply" %>
|
20
|
+
# <% end %>
|
21
|
+
# <% end %>
|
22
|
+
#
|
23
|
+
#
|
24
|
+
# Additionally the component allows for access to the dropdown that displays when interacting with the input.
|
25
|
+
# The dropdown supports additional "footer" content to be added.
|
26
|
+
#
|
27
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
|
28
|
+
# <% filter.with_modal(collection:) do |modal| %>
|
29
|
+
# <% modal.with_footer do %>
|
30
|
+
# <%= link_to "Docs", docs_path %>
|
31
|
+
# <% end %>
|
32
|
+
# <% end %>
|
33
|
+
# <% end %>
|
34
|
+
#
|
35
|
+
class FilterComponent < ViewComponent::Base
|
36
|
+
include Katalyst::HtmlAttributes
|
37
|
+
include Katalyst::Tables::Frontend
|
38
|
+
|
39
|
+
renders_one :modal, Katalyst::Tables::Filter::ModalComponent
|
40
|
+
|
41
|
+
define_html_attribute_methods :input_attributes
|
42
|
+
|
43
|
+
attr_reader :collection, :url
|
44
|
+
|
45
|
+
def initialize(collection:, url:, **)
|
46
|
+
super(**)
|
47
|
+
|
48
|
+
@collection = collection
|
49
|
+
@url = url
|
50
|
+
end
|
51
|
+
|
52
|
+
def before_render
|
53
|
+
with_modal(collection:) unless modal?
|
54
|
+
end
|
55
|
+
|
56
|
+
def form(url: @url, **options, &)
|
57
|
+
form_with(model: collection,
|
58
|
+
url:,
|
59
|
+
method: :get,
|
60
|
+
**options) do |form|
|
61
|
+
concat(form.hidden_field(:sort))
|
62
|
+
|
63
|
+
yield form if block_given?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def default_html_attributes
|
70
|
+
{
|
71
|
+
data: {
|
72
|
+
controller: "tables--filter--modal",
|
73
|
+
action: <<~ACTIONS.gsub(/\s+/, " "),
|
74
|
+
click@window->tables--filter--modal#close
|
75
|
+
click->tables--filter--modal#open:stop
|
76
|
+
keydown.esc->tables--filter--modal#close
|
77
|
+
ACTIONS
|
78
|
+
},
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def default_input_attributes
|
83
|
+
{
|
84
|
+
data: {
|
85
|
+
action: "focus->tables--filter--modal#open",
|
86
|
+
},
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -69,6 +69,14 @@ module Katalyst
|
|
69
69
|
render(component, &)
|
70
70
|
end
|
71
71
|
|
72
|
+
# Construct a new filter.
|
73
|
+
#
|
74
|
+
# @param collection [Katalyst::Tables::Collection::Core] the collection to render
|
75
|
+
# @param url [String] the url to submit the form to (e.g. <resources>_path)
|
76
|
+
def filter_with(collection:, url: url_for(action: :index), &)
|
77
|
+
render(FilterComponent.new(collection:, url:), &)
|
78
|
+
end
|
79
|
+
|
72
80
|
private
|
73
81
|
|
74
82
|
def default_table_component_class
|
@@ -3,6 +3,7 @@ import OrderableListController from "./orderable/list_controller";
|
|
3
3
|
import OrderableFormController from "./orderable/form_controller";
|
4
4
|
import SelectionFormController from "./selection/form_controller";
|
5
5
|
import SelectionItemController from "./selection/item_controller";
|
6
|
+
import FilterModalController from "./filter/modal_controller";
|
6
7
|
|
7
8
|
const Definitions = [
|
8
9
|
{
|
@@ -25,6 +26,10 @@ const Definitions = [
|
|
25
26
|
identifier: "tables--selection--item",
|
26
27
|
controllerConstructor: SelectionItemController,
|
27
28
|
},
|
29
|
+
{
|
30
|
+
identifier: "tables--filter--modal",
|
31
|
+
controllerConstructor: FilterModalController,
|
32
|
+
},
|
28
33
|
];
|
29
34
|
|
30
35
|
export { Definitions as default };
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class FilterModalController extends Controller {
|
4
|
+
static targets = ["modal"];
|
5
|
+
|
6
|
+
close(e) {
|
7
|
+
delete this.modalTarget.dataset.open;
|
8
|
+
}
|
9
|
+
|
10
|
+
open(e) {
|
11
|
+
this.modalTarget.dataset.open = "true";
|
12
|
+
}
|
13
|
+
}
|
@@ -25,6 +25,10 @@ module Katalyst
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
+
|
29
|
+
def enum_attribute?(key)
|
30
|
+
_default_attributes[key].value.is_a?(::Array)
|
31
|
+
end
|
28
32
|
end
|
29
33
|
|
30
34
|
included do
|
@@ -39,11 +43,21 @@ module Katalyst
|
|
39
43
|
clear_changes_information
|
40
44
|
end
|
41
45
|
|
46
|
+
# Collections are filtered when any parameters have changed from their defaults.
|
47
|
+
def filtered?
|
48
|
+
filters.any?
|
49
|
+
end
|
50
|
+
|
42
51
|
# Collections that do not include Sorting are never sortable.
|
43
52
|
def sortable?
|
44
53
|
false
|
45
54
|
end
|
46
55
|
|
56
|
+
# Collections that do not include Query are never searchable.
|
57
|
+
def searchable?
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
47
61
|
def apply(items)
|
48
62
|
@items = items
|
49
63
|
reducers.build do |_|
|
@@ -52,6 +66,22 @@ module Katalyst
|
|
52
66
|
end.call(self)
|
53
67
|
self
|
54
68
|
end
|
69
|
+
|
70
|
+
def filter
|
71
|
+
# no-op by default
|
72
|
+
end
|
73
|
+
|
74
|
+
def filters
|
75
|
+
changes.except("sort", "page", "query").transform_values(&:second)
|
76
|
+
end
|
77
|
+
|
78
|
+
def model
|
79
|
+
if items < ActiveRecord::Base
|
80
|
+
items
|
81
|
+
else
|
82
|
+
items.model
|
83
|
+
end
|
84
|
+
end
|
55
85
|
end
|
56
86
|
end
|
57
87
|
end
|
@@ -3,19 +3,90 @@
|
|
3
3
|
module Katalyst
|
4
4
|
module Tables
|
5
5
|
module Collection
|
6
|
-
|
6
|
+
module Filtering
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
def filter
|
10
|
-
# no-op by default
|
11
|
-
end
|
9
|
+
DEFAULT_ATTRIBUTES = %w[sort page query].freeze
|
12
10
|
|
13
|
-
|
14
|
-
|
11
|
+
included do
|
12
|
+
use(Filter)
|
15
13
|
end
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
class Filter
|
16
|
+
include ActiveRecord::Sanitization::ClassMethods
|
17
|
+
|
18
|
+
def initialize(app)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(collection)
|
23
|
+
collection.class._default_attributes.each_value do |attribute|
|
24
|
+
key = attribute.name
|
25
|
+
|
26
|
+
next if DEFAULT_ATTRIBUTES.include?(key)
|
27
|
+
|
28
|
+
value = collection.attributes[key]
|
29
|
+
|
30
|
+
filter_attribute(collection, key, value, attribute.type.type)
|
31
|
+
end
|
32
|
+
|
33
|
+
@app.call(collection)
|
34
|
+
end
|
35
|
+
|
36
|
+
def filter_attribute(collection, key, value, type)
|
37
|
+
if key == "search"
|
38
|
+
search(collection, value)
|
39
|
+
elsif type == :string
|
40
|
+
filter_matches(collection, key, value)
|
41
|
+
elsif type == :boolean
|
42
|
+
filter_eq(collection, key, value) unless value.nil?
|
43
|
+
elsif value.present?
|
44
|
+
filter_eq(collection, key, value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def search(collection, search)
|
49
|
+
return if search.blank? || !collection.searchable?
|
50
|
+
|
51
|
+
collection.items = collection.items.public_send(collection.config.search_scope, search)
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter_matches(collection, key, value)
|
55
|
+
return if value.nil?
|
56
|
+
|
57
|
+
model, column = join_key(collection, key)
|
58
|
+
arel_column = model.arel_table[column]
|
59
|
+
|
60
|
+
collection.items = collection.items.where(arel_column.matches("%#{sanitize_sql_like(value)}%"))
|
61
|
+
end
|
62
|
+
|
63
|
+
def filter_eq(collection, key, value)
|
64
|
+
model, column = join_key(collection, key)
|
65
|
+
|
66
|
+
condition = if model.attribute_types.has_key?(column)
|
67
|
+
model.where(column => value)
|
68
|
+
else
|
69
|
+
model.public_send(column, value)
|
70
|
+
end
|
71
|
+
|
72
|
+
collection.items = collection.items.merge(condition)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def join_key(collection, key)
|
78
|
+
if key.include?(".")
|
79
|
+
table, column = key.split(".")
|
80
|
+
collection.items = collection.items.joins(table.to_sym)
|
81
|
+
[collection.items.reflections[table].klass, column]
|
82
|
+
else
|
83
|
+
[collection.items.model, key]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def column_for(key)
|
88
|
+
key.include?(".") ? key.split(".").last : key
|
89
|
+
end
|
19
90
|
end
|
20
91
|
end
|
21
92
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class ArrayValueParser < ValueParser
|
8
|
+
# @param query [StringScanner]
|
9
|
+
def parse(query)
|
10
|
+
@query = query
|
11
|
+
|
12
|
+
skip_whitespace
|
13
|
+
|
14
|
+
if query.scan(/#{'\['}/)
|
15
|
+
take_values
|
16
|
+
else
|
17
|
+
take_value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def take_values
|
22
|
+
until query.eos?
|
23
|
+
skip_whitespace
|
24
|
+
break unless take_quoted_value || take_unquoted_value
|
25
|
+
|
26
|
+
skip_whitespace
|
27
|
+
break unless take_delimiter
|
28
|
+
end
|
29
|
+
|
30
|
+
skip_whitespace
|
31
|
+
take_end_of_list
|
32
|
+
end
|
33
|
+
|
34
|
+
def take_value
|
35
|
+
take_quoted_value || take_unquoted_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def take_delimiter
|
39
|
+
query.scan(/#{','}/)
|
40
|
+
end
|
41
|
+
|
42
|
+
def take_end_of_list
|
43
|
+
query.scan(/#{']'}/)
|
44
|
+
end
|
45
|
+
|
46
|
+
def value=(value)
|
47
|
+
return if @attribute.type_cast(value).nil? # undefined attribute
|
48
|
+
|
49
|
+
current = @collection.attributes[@attribute.name]
|
50
|
+
@collection.assign_attributes(@attribute.name => current + [value])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class Parser # :nodoc:
|
8
|
+
# query [StringScanner]
|
9
|
+
attr_accessor :query
|
10
|
+
attr_reader :collection, :untagged
|
11
|
+
|
12
|
+
def initialize(collection)
|
13
|
+
@collection = collection
|
14
|
+
@untagged = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param query [String]
|
18
|
+
def parse(query)
|
19
|
+
@query = StringScanner.new(query)
|
20
|
+
|
21
|
+
until @query.eos?
|
22
|
+
skip_whitespace
|
23
|
+
|
24
|
+
# break to ensure we don't loop indefinitely on bad input
|
25
|
+
break unless take_tagged || take_untagged
|
26
|
+
end
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def skip_whitespace
|
34
|
+
query.scan(/\s+/)
|
35
|
+
end
|
36
|
+
|
37
|
+
def take_tagged
|
38
|
+
return unless query.scan(/(\w+(\.\w+)?):/)
|
39
|
+
|
40
|
+
key, = query.values_at(1)
|
41
|
+
parser_for(key).parse(query)
|
42
|
+
end
|
43
|
+
|
44
|
+
def take_untagged
|
45
|
+
return unless query.scan(/\S+/)
|
46
|
+
|
47
|
+
untagged << query.matched
|
48
|
+
|
49
|
+
untagged
|
50
|
+
end
|
51
|
+
|
52
|
+
def parser_for(key)
|
53
|
+
attribute = collection.class._default_attributes[key]
|
54
|
+
|
55
|
+
if collection.class.enum_attribute?(key)
|
56
|
+
ArrayValueParser.new(collection:, attribute:)
|
57
|
+
else
|
58
|
+
SingleValueParser.new(collection:, attribute:)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class SingleValueParser < ValueParser
|
8
|
+
# @param query [StringScanner]
|
9
|
+
def parse(query)
|
10
|
+
@query = query
|
11
|
+
|
12
|
+
take_quoted_value || take_unquoted_value
|
13
|
+
end
|
14
|
+
|
15
|
+
def value=(value)
|
16
|
+
return if @attribute.type_cast(value).nil? # undefined attribute
|
17
|
+
|
18
|
+
@collection.assign_attributes(@attribute.name => value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class ValueParser
|
8
|
+
attr_accessor :query
|
9
|
+
|
10
|
+
def initialize(collection:, attribute:)
|
11
|
+
@collection = collection
|
12
|
+
@attribute = attribute
|
13
|
+
end
|
14
|
+
|
15
|
+
def take_quoted_value
|
16
|
+
return unless query.scan(/"([^"]*)"/)
|
17
|
+
|
18
|
+
self.value, = query.values_at(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def take_unquoted_value
|
22
|
+
return unless query.scan(/([^" \],]*)/)
|
23
|
+
|
24
|
+
self.value, = query.values_at(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
def skip_whitespace
|
28
|
+
query.scan(/\s+/)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query # :nodoc:
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include Filtering
|
10
|
+
|
11
|
+
included do
|
12
|
+
config_accessor :search_scope
|
13
|
+
|
14
|
+
attribute :query, :string, default: ""
|
15
|
+
attribute :search, :string, default: ""
|
16
|
+
|
17
|
+
# Note: this is defined inline so that we can overwrite query=
|
18
|
+
def query=(value)
|
19
|
+
query = super
|
20
|
+
|
21
|
+
parser = Parser.new(self).parse(query)
|
22
|
+
|
23
|
+
if searchable? && parser.untagged.any?
|
24
|
+
self.search = parser.untagged.join(" ")
|
25
|
+
end
|
26
|
+
|
27
|
+
query
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns true if the collection supports untagged searching. This
|
32
|
+
# requires config.search_scope to be set to the name of the scope to use
|
33
|
+
# in the target record for untagged text searches. If not set, untagged
|
34
|
+
# search terms will be silently ignored.
|
35
|
+
#
|
36
|
+
# @return [true, false]
|
37
|
+
def searchable?
|
38
|
+
config.search_scope.present?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -23,7 +23,6 @@ module Katalyst
|
|
23
23
|
# ````
|
24
24
|
class Base
|
25
25
|
include Core
|
26
|
-
include Filtering
|
27
26
|
include Pagination
|
28
27
|
include Sorting
|
29
28
|
|
@@ -34,10 +33,6 @@ module Katalyst
|
|
34
33
|
new.with_params(params)
|
35
34
|
end
|
36
35
|
|
37
|
-
def model
|
38
|
-
items.model
|
39
|
-
end
|
40
|
-
|
41
36
|
def model_name
|
42
37
|
@model_name ||= items.model_name.dup.tap do |name|
|
43
38
|
name.param_key = ""
|
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: 3.
|
4
|
+
version: 3.2.0
|
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-06-
|
11
|
+
date: 2024-06-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: katalyst-html-attributes
|
@@ -54,14 +54,17 @@ files:
|
|
54
54
|
- app/assets/builds/katalyst/tables.min.js
|
55
55
|
- app/assets/builds/katalyst/tables.min.js.map
|
56
56
|
- app/assets/config/katalyst-tables.js
|
57
|
+
- app/assets/stylesheets/katalyst/tables/_filter.scss
|
57
58
|
- app/assets/stylesheets/katalyst/tables/_index.scss
|
58
59
|
- app/assets/stylesheets/katalyst/tables/_ordinal.scss
|
60
|
+
- app/assets/stylesheets/katalyst/tables/_select.scss
|
59
61
|
- app/assets/stylesheets/katalyst/tables/_summary.scss
|
60
62
|
- app/assets/stylesheets/katalyst/tables/_table.scss
|
61
63
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss
|
62
64
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss
|
63
65
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss
|
64
66
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss
|
67
|
+
- app/assets/stylesheets/katalyst/tables/typed-columns/_enum.scss
|
65
68
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss
|
66
69
|
- app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss
|
67
70
|
- app/components/concerns/katalyst/tables/has_table_content.rb
|
@@ -81,6 +84,7 @@ files:
|
|
81
84
|
- app/components/katalyst/tables/cells/currency_component.rb
|
82
85
|
- app/components/katalyst/tables/cells/date_component.rb
|
83
86
|
- app/components/katalyst/tables/cells/date_time_component.rb
|
87
|
+
- app/components/katalyst/tables/cells/enum_component.rb
|
84
88
|
- app/components/katalyst/tables/cells/number_component.rb
|
85
89
|
- app/components/katalyst/tables/cells/ordinal_component.rb
|
86
90
|
- app/components/katalyst/tables/cells/rich_text_component.rb
|
@@ -88,6 +92,10 @@ files:
|
|
88
92
|
- app/components/katalyst/tables/data.rb
|
89
93
|
- app/components/katalyst/tables/empty_caption_component.html.erb
|
90
94
|
- app/components/katalyst/tables/empty_caption_component.rb
|
95
|
+
- app/components/katalyst/tables/filter/modal_component.html.erb
|
96
|
+
- app/components/katalyst/tables/filter/modal_component.rb
|
97
|
+
- app/components/katalyst/tables/filter_component.html.erb
|
98
|
+
- app/components/katalyst/tables/filter_component.rb
|
91
99
|
- app/components/katalyst/tables/header_row_component.html.erb
|
92
100
|
- app/components/katalyst/tables/header_row_component.rb
|
93
101
|
- app/components/katalyst/tables/label.rb
|
@@ -104,6 +112,7 @@ files:
|
|
104
112
|
- app/controllers/concerns/katalyst/tables/backend.rb
|
105
113
|
- app/helpers/katalyst/tables/frontend.rb
|
106
114
|
- app/javascript/tables/application.js
|
115
|
+
- app/javascript/tables/filter/modal_controller.js
|
107
116
|
- app/javascript/tables/orderable/form_controller.js
|
108
117
|
- app/javascript/tables/orderable/item_controller.js
|
109
118
|
- app/javascript/tables/orderable/list_controller.js
|
@@ -113,6 +122,11 @@ files:
|
|
113
122
|
- app/models/concerns/katalyst/tables/collection/filtering.rb
|
114
123
|
- app/models/concerns/katalyst/tables/collection/has_params.rb
|
115
124
|
- app/models/concerns/katalyst/tables/collection/pagination.rb
|
125
|
+
- app/models/concerns/katalyst/tables/collection/query.rb
|
126
|
+
- app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb
|
127
|
+
- app/models/concerns/katalyst/tables/collection/query/parser.rb
|
128
|
+
- app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb
|
129
|
+
- app/models/concerns/katalyst/tables/collection/query/value_parser.rb
|
116
130
|
- app/models/concerns/katalyst/tables/collection/reducers.rb
|
117
131
|
- app/models/concerns/katalyst/tables/collection/sorting.rb
|
118
132
|
- app/models/katalyst/tables/collection/array.rb
|
@@ -147,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
161
|
- !ruby/object:Gem::Version
|
148
162
|
version: '0'
|
149
163
|
requirements: []
|
150
|
-
rubygems_version: 3.5.
|
164
|
+
rubygems_version: 3.5.11
|
151
165
|
signing_key:
|
152
166
|
specification_version: 4
|
153
167
|
summary: HTML table generator for Rails views
|