katalyst-tables 3.1.0 → 3.2.0
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 +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":"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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/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
|