katalyst-navigation 1.4.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -5
  3. data/app/assets/builds/katalyst/navigation.esm.js +920 -0
  4. data/app/assets/builds/katalyst/navigation.js +920 -0
  5. data/app/assets/builds/katalyst/navigation.min.js +2 -0
  6. data/app/assets/builds/katalyst/navigation.min.js.map +1 -0
  7. data/app/assets/config/katalyst-navigation.js +1 -1
  8. data/app/assets/stylesheets/katalyst/navigation/editor/_index.scss +2 -2
  9. data/app/assets/stylesheets/katalyst/navigation/editor/_new-items.scss +32 -10
  10. data/app/components/katalyst/navigation/editor/base_component.rb +48 -0
  11. data/app/components/katalyst/navigation/editor/errors_component.html.erb +12 -0
  12. data/app/components/katalyst/navigation/editor/errors_component.rb +15 -0
  13. data/app/components/katalyst/navigation/editor/item_component.html.erb +27 -0
  14. data/app/components/katalyst/navigation/editor/item_component.rb +30 -0
  15. data/app/components/katalyst/navigation/editor/item_editor_component.rb +51 -0
  16. data/app/components/katalyst/navigation/editor/new_item_component.html.erb +14 -0
  17. data/app/components/katalyst/navigation/editor/new_item_component.rb +49 -0
  18. data/app/components/katalyst/navigation/editor/new_items_component.html.erb +3 -0
  19. data/app/components/katalyst/navigation/editor/new_items_component.rb +20 -0
  20. data/app/{views/katalyst/navigation/menus/_list_item.html.erb → components/katalyst/navigation/editor/row_component.html.erb} +1 -1
  21. data/{lib/katalyst/navigation/version.rb → app/components/katalyst/navigation/editor/row_component.rb} +4 -1
  22. data/app/{helpers/katalyst/navigation/editor/status_bar.rb → components/katalyst/navigation/editor/status_bar_component.rb} +17 -13
  23. data/app/components/katalyst/navigation/editor/table_component.html.erb +11 -0
  24. data/app/components/katalyst/navigation/editor/table_component.rb +36 -0
  25. data/app/components/katalyst/navigation/editor_component.html.erb +9 -0
  26. data/app/components/katalyst/navigation/editor_component.rb +47 -0
  27. data/app/controllers/concerns/katalyst/navigation/has_navigation.rb +5 -14
  28. data/app/controllers/katalyst/navigation/items_controller.rb +28 -12
  29. data/app/controllers/katalyst/navigation/menus_controller.rb +25 -13
  30. data/app/helpers/katalyst/navigation/frontend_helper.rb +7 -7
  31. data/app/javascript/navigation/application.js +30 -0
  32. data/app/{assets/javascripts/utils → javascript}/navigation/editor/item.js +0 -1
  33. data/app/{assets/javascripts/controllers → javascript}/navigation/editor/item_controller.js +1 -1
  34. data/app/{assets/javascripts/controllers → javascript}/navigation/editor/list_controller.js +12 -2
  35. data/app/{assets/javascripts/utils → javascript}/navigation/editor/menu.js +1 -1
  36. data/app/{assets/javascripts/controllers → javascript}/navigation/editor/menu_controller.js +3 -3
  37. data/app/models/katalyst/navigation/menu.rb +2 -0
  38. data/app/models/katalyst/navigation/types/nodes_type.rb +2 -2
  39. data/app/views/katalyst/navigation/items/_button.html.erb +6 -0
  40. data/app/views/katalyst/navigation/items/_form_errors.html.erb +5 -0
  41. data/app/views/katalyst/navigation/items/_heading.html.erb +1 -0
  42. data/app/views/katalyst/navigation/items/_link.html.erb +11 -0
  43. data/app/views/katalyst/navigation/items/edit.html.erb +4 -3
  44. data/app/views/katalyst/navigation/items/edit.turbo_stream.erb +3 -0
  45. data/app/views/katalyst/navigation/items/new.html.erb +1 -1
  46. data/app/views/katalyst/navigation/items/update.turbo_stream.erb +2 -5
  47. data/app/views/katalyst/navigation/menus/edit.html.erb +1 -1
  48. data/app/views/katalyst/navigation/menus/index.html.erb +1 -1
  49. data/app/views/katalyst/navigation/menus/show.html.erb +3 -13
  50. data/config/importmap.rb +1 -4
  51. data/lib/katalyst/navigation/config.rb +4 -0
  52. data/lib/katalyst/navigation/engine.rb +1 -1
  53. data/lib/katalyst/navigation.rb +6 -1
  54. data/spec/factories/katalyst/navigation/menus.rb +1 -1
  55. metadata +93 -27
  56. data/app/controllers/katalyst/navigation/base_controller.rb +0 -12
  57. data/app/helpers/katalyst/navigation/editor/base.rb +0 -41
  58. data/app/helpers/katalyst/navigation/editor/errors.rb +0 -24
  59. data/app/helpers/katalyst/navigation/editor/item.rb +0 -62
  60. data/app/helpers/katalyst/navigation/editor/list.rb +0 -41
  61. data/app/helpers/katalyst/navigation/editor/menu.rb +0 -47
  62. data/app/helpers/katalyst/navigation/editor/new_item.rb +0 -53
  63. data/app/helpers/katalyst/navigation/editor_helper.rb +0 -52
  64. data/app/views/katalyst/navigation/menus/_item.html.erb +0 -15
  65. data/app/views/katalyst/navigation/menus/_new_item.html.erb +0 -3
  66. data/app/views/katalyst/navigation/menus/_new_items.html.erb +0 -5
  67. /data/app/{assets/javascripts/controllers → javascript}/navigation/editor/new_item_controller.js +0 -0
  68. /data/app/{assets/javascripts/utils → javascript}/navigation/editor/rules-engine.js +0 -0
  69. /data/app/{assets/javascripts/controllers → javascript}/navigation/editor/status_bar_controller.js +0 -0
@@ -0,0 +1,2 @@
1
+ import{Controller as e}from"@hotwired/stimulus";class t{static comparator(e,t){return e.index-t.index}constructor(e){this.node=e}get itemId(){return this.node.dataset.navigationItemId}get#e(){return this.node.querySelector('input[name$="[id]"]')}set itemId(e){this.itemId!==e&&(this.node.dataset.navigationItemId=`${e}`,this.#e.value=`${e}`)}get depth(){return parseInt(this.node.dataset.navigationDepth)||0}get#t(){return this.node.querySelector('input[name$="[depth]"]')}set depth(e){this.depth!==e&&(this.node.dataset.navigationDepth=`${e}`,this.#t.value=`${e}`)}get index(){return parseInt(this.node.dataset.navigationIndex)}get#n(){return this.node.querySelector('input[name$="[index]"]')}set index(e){this.index!==e&&(this.node.dataset.navigationIndex=`${e}`,this.#n.value=`${e}`)}get isLayout(){return this.node.hasAttribute("data-content-layout")}get previousItem(){let e=this.node.previousElementSibling;if(e)return new t(e)}get nextItem(){let e=this.node.nextElementSibling;if(e)return new t(e)}hasCollapsedDescendants(){let e=this.#s;return!!e&&e.children.length>0}hasExpandedDescendants(){let e=this.nextItem;return!!e&&e.depth>this.depth}traverse(e){const t=this.#a;e(this),this.#i(e),t.forEach((t=>t.#i(e)))}#i(e){this.hasCollapsedDescendants()&&this.#d.forEach((t=>{e(t),t.#i(e)}))}collapse(){let e=this.#s;e||(e=function(e){const t=document.createElement("ol");return t.setAttribute("class","hidden"),t.dataset.navigationChildren="",e.appendChild(t),t}(this.node)),this.#a.forEach((t=>e.appendChild(t.node)))}expand(){this.hasCollapsedDescendants()&&Array.from(this.#s.children).reverse().forEach((e=>{this.node.insertAdjacentElement("afterend",e)}))}toggleRule(e,t=!1){this.node.dataset.hasOwnProperty(e)&&!t&&delete this.node.dataset[e],!this.node.dataset.hasOwnProperty(e)&&t&&(this.node.dataset[e]=""),"denyDrag"===e&&(this.node.hasAttribute("draggable")||t||this.node.setAttribute("draggable","true"),this.node.hasAttribute("draggable")&&t&&this.node.removeAttribute("draggable"))}hasItemIdChanged(){return!(this.#e.value===this.itemId)}updateAfterChange(){this.itemId=this.#e.value,this.#n.value=this.index,this.#t.value=this.depth}get#s(){return this.node.querySelector(":scope > [data-navigation-children]")}get#a(){const e=[];let t=this.nextItem;for(;t&&t.depth>this.depth;)e.push(t),t=t.nextItem;return e}get#d(){return this.hasCollapsedDescendants()?Array.from(this.#s.children).map((e=>new t(e))):[]}}class n{constructor(e){this.node=e}get items(){return e=this.node.querySelectorAll("[data-navigation-index]"),Array.from(e).map((e=>new t(e)));var e}get state(){const e=this.node.querySelectorAll("li input[type=hidden]");return Array.from(e).map((e=>e.value)).join("/")}reindex(){this.items.map(((e,t)=>e.index=t))}reset(){this.items.sort(t.comparator).forEach((e=>{this.node.appendChild(e.node)}))}}class s{static rules=["denyDeNest","denyNest","denyCollapse","denyExpand","denyRemove","denyDrag","denyEdit"];constructor(e=null,t=!1){this.maxDepth=e,this.debug=t?(...e)=>console.log(...e):()=>{}}normalize(e){this.firstItemDepthZero(e),this.depthMustBeSet(e),this.itemCannotHaveInvalidDepth(e),this.itemCannotExceedDepthLimit(e),this.parentMustBeLayout(e),this.parentCannotHaveExpandedAndCollapsedChildren(e)}update(e){this.rules={},this.parentsCannotDeNest(e),this.rootsCannotDeNest(e),this.nestingNeedsParent(e),this.nestingCannotExceedMaxDepth(e),this.leavesCannotCollapse(e),this.needHiddenItemsToExpand(e),this.parentsCannotBeDeleted(e),this.parentsCannotBeDragged(e),s.rules.forEach((t=>{e.toggleRule(t,!!this.rules[t])}))}firstItemDepthZero(e){0===e.index&&0!==e.depth&&(this.debug(`enforce depth on item ${e.index}: ${e.depth} => 0`),e.depth=0)}depthMustBeSet(e){(isNaN(e.depth)||e.depth<0)&&(this.debug(`unset depth on item ${e.index}: => 0`),e.depth=0)}itemCannotHaveInvalidDepth(e){const t=e.previousItem;t&&t.depth<e.depth-1&&(this.debug(`invalid depth on item ${e.index}: ${e.depth} => ${t.depth+1}`),e.depth=t.depth+1)}itemCannotExceedDepthLimit(e){this.maxDepth>0&&this.maxDepth<=e.depth&&(e.depth=this.maxDepth-1)}parentMustBeLayout(e){const t=e.previousItem;t&&t.depth<e.depth&&!t.isLayout&&(this.debug(`invalid parent for item ${e.index}: ${e.depth} => ${t.depth}`),e.depth=t.depth)}parentCannotHaveExpandedAndCollapsedChildren(e){e.hasCollapsedDescendants()&&e.hasExpandedDescendants()&&(this.debug(`expanding collapsed children of item ${e.index}`),e.expand())}parentsCannotDeNest(e){e.hasExpandedDescendants()&&this.#r("denyDeNest")}rootsCannotDeNest(e){0===e.depth&&this.#r("denyDeNest")}leavesCannotCollapse(e){e.hasExpandedDescendants()||this.#r("denyCollapse")}needHiddenItemsToExpand(e){e.hasCollapsedDescendants()||this.#r("denyExpand")}nestingNeedsParent(e){const t=e.previousItem;t?t.depth<e.depth?this.#r("denyNest"):t.depth!==e.depth||t.isLayout||this.#r("denyNest"):this.#r("denyNest")}nestingCannotExceedMaxDepth(e){this.maxDepth>0&&this.maxDepth<=e.depth+1&&this.#r("denyNest")}parentsCannotBeDeleted(e){e.itemId&&!e.hasExpandedDescendants()||this.#r("denyRemove")}parentsCannotBeDragged(e){e.hasExpandedDescendants()&&this.#r("denyDrag")}#r(e){this.rules[e]=!0}}function a(e){return new t(e.target.closest("[data-navigation-item]"))}function i(e,t){if(e&&e!==t){if("LI"===e.nodeName){const n=e.compareDocumentPosition(t);n&Node.DOCUMENT_POSITION_FOLLOWING?e.insertAdjacentElement("beforebegin",t):n&Node.DOCUMENT_POSITION_PRECEDING&&e.insertAdjacentElement("afterend",t)}"OL"===e.nodeName&&e.appendChild(t)}}const d=[{identifier:"navigation--editor--menu",controllerConstructor:class extends e{static targets=["menu"];static values={maxDepth:Number};connect(){this.state=this.menu.state,this.reindex()}get menu(){return new n(this.menuTarget)}reindex(){this.menu.reindex(),this.#h()}reset(){this.menu.reset()}drop(e){this.menu.reindex();const t=a(e),n=t.previousItem;let s=0;s=void 0===n?-t.depth:t.nextItem&&t.nextItem.depth>n.depth?n.depth-t.depth+1:n.depth-t.depth,t.traverse((e=>{e.depth+=s})),this.#h(),e.preventDefault()}remove(e){a(e).node.remove(),this.#h(),e.preventDefault()}nest(e){a(e).traverse((e=>{e.depth+=1})),this.#h(),e.preventDefault()}deNest(e){a(e).traverse((e=>{e.depth-=1})),this.#h(),e.preventDefault()}collapse(e){a(e).collapse(),this.#h(),e.preventDefault()}expand(e){a(e).expand(),this.#h(),e.preventDefault()}#h(){this.updateRequested=!0,setTimeout((()=>{if(!this.updateRequested)return;this.updateRequested=!1;const e=new s(this.maxDepthValue);this.menu.items.forEach((t=>e.normalize(t))),this.menu.items.forEach((t=>e.update(t))),this.#o()}),0)}#o(){this.dispatch("change",{bubbles:!0,prefix:"navigation",detail:{dirty:this.#l()}})}#l(){return this.menu.state!==this.state}}},{identifier:"navigation--editor--item",controllerConstructor:class extends e{get item(){return new t(this.li)}get ol(){return this.element.closest("ol")}get li(){return this.element.closest("li")}connect(){this.element.dataset.hasOwnProperty("delete")?this.remove():this.item.index>=0?this.item.hasItemIdChanged()&&(this.item.updateAfterChange(),this.reindex()):this.reindex()}remove(){this.ol,this.li.remove(),this.reindex()}reindex(){this.dispatch("reindex",{bubbles:!0,prefix:"navigation"})}}},{identifier:"navigation--editor--list",controllerConstructor:class extends e{dragstart(e){if(this.element!==e.target.parentElement)return;const t=e.target;e.dataTransfer.effectAllowed="move",setTimeout((()=>t.dataset.dragging=""))}dragover(e){const t=this.dragItem();if(t)return i(this.dropTarget(e.target),t),e.preventDefault(),!0}dragenter(e){if(e.preventDefault(),"copy"===e.dataTransfer.effectAllowed&&!this.dragItem()){const e=document.createElement("li");e.dataset.dragging="",e.dataset.newItem="",this.element.prepend(e)}}dragleave(e){const t=this.dragItem(),n=this.dropTarget(e.relatedTarget);t&&!n&&t.dataset.hasOwnProperty("newItem")&&t.remove()}drop(e){let t=this.dragItem();if(t){if(e.preventDefault(),delete t.dataset.dragging,i(this.dropTarget(e.target),t),t.dataset.hasOwnProperty("newItem")){const n=t,s=document.createElement("template");s.innerHTML=e.dataTransfer.getData("text/html"),t=s.content.querySelector("li"),this.element.replaceChild(t,n),setTimeout((()=>t.querySelector("[role='button'][value='edit']").click()))}this.dispatch("drop",{target:t,bubbles:!0,prefix:"navigation"})}}dragend(){const e=this.dragItem();e&&(delete e.dataset.dragging,this.reset())}dragItem(){return this.element.querySelector("[data-dragging]")}dropTarget(e){return e.closest("[data-controller='navigation--editor--list'] > *")||e.closest("[data-controller='navigation--editor--list']")}reindex(){this.dispatch("reindex",{bubbles:!0,prefix:"navigation"})}reset(){this.dispatch("reset",{bubbles:!0,prefix:"navigation"})}}},{identifier:"navigation--editor--new-item",controllerConstructor:class extends e{static targets=["template"];dragstart(e){this.element===e.target&&(e.dataTransfer.setData("text/html",this.templateTarget.innerHTML),e.dataTransfer.effectAllowed="copy")}}},{identifier:"navigation--editor--status-bar",controllerConstructor:class extends e{connect(){this.versionState=this.element.dataset.state}change(e){e.detail&&e.detail.hasOwnProperty("dirty")&&this.update(e.detail)}update({dirty:e}){this.element.dataset.state=e?"dirty":this.versionState}}}];export{d as default};
2
+ //# sourceMappingURL=navigation.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.min.js","sources":["../../../javascript/navigation/editor/item.js","../../../javascript/navigation/editor/menu.js","../../../javascript/navigation/editor/rules-engine.js","../../../javascript/navigation/editor/menu_controller.js","../../../javascript/navigation/editor/list_controller.js","../../../javascript/navigation/application.js","../../../javascript/navigation/editor/item_controller.js","../../../javascript/navigation/editor/new_item_controller.js","../../../javascript/navigation/editor/status_bar_controller.js"],"sourcesContent":["export default class Item {\n /**\n * Sort items by their index.\n *\n * @param a {Item}\n * @param b {Item}\n * @returns {number}\n */\n static comparator(a, b) {\n return a.index - b.index;\n }\n\n /**\n * @param node {Element} li[data-navigation-index]\n */\n constructor(node) {\n this.node = node;\n }\n\n /**\n * @returns {String} id of the node's item (from data attributes)\n */\n get itemId() {\n return this.node.dataset[`navigationItemId`];\n }\n\n get #itemIdInput() {\n return this.node.querySelector(`input[name$=\"[id]\"]`);\n }\n\n /**\n * @param itemId {String} id\n */\n set itemId(id) {\n if (this.itemId === id) return;\n\n this.node.dataset[`navigationItemId`] = `${id}`;\n this.#itemIdInput.value = `${id}`;\n }\n\n /**\n * @returns {number} logical nesting depth of node in menu\n */\n get depth() {\n return parseInt(this.node.dataset[`navigationDepth`]) || 0;\n }\n\n get #depthInput() {\n return this.node.querySelector(`input[name$=\"[depth]\"]`);\n }\n\n /**\n * @param depth {number} depth >= 0\n */\n set depth(depth) {\n if (this.depth === depth) return;\n\n this.node.dataset[`navigationDepth`] = `${depth}`;\n this.#depthInput.value = `${depth}`;\n }\n\n /**\n * @returns {number} logical index of node in menu (pre-order traversal)\n */\n get index() {\n return parseInt(this.node.dataset[`navigationIndex`]);\n }\n\n get #indexInput() {\n return this.node.querySelector(`input[name$=\"[index]\"]`);\n }\n\n /**\n * @param index {number} index >= 0\n */\n set index(index) {\n if (this.index === index) return;\n\n this.node.dataset[`navigationIndex`] = `${index}`;\n this.#indexInput.value = `${index}`;\n }\n\n /**\n * @returns {boolean} true if this item can have children\n */\n get isLayout() {\n return this.node.hasAttribute(\"data-content-layout\");\n }\n\n /**\n * @returns {Item} nearest neighbour (index - 1)\n */\n get previousItem() {\n let sibling = this.node.previousElementSibling;\n if (sibling) return new Item(sibling);\n }\n\n /**\n * @returns {Item} nearest neighbour (index + 1)\n */\n get nextItem() {\n let sibling = this.node.nextElementSibling;\n if (sibling) return new Item(sibling);\n }\n\n /**\n * @returns {boolean} true if this item has any collapsed children\n */\n hasCollapsedDescendants() {\n let childrenList = this.#childrenListElement;\n return !!childrenList && childrenList.children.length > 0;\n }\n\n /**\n * @returns {boolean} true if this item has any expanded children\n */\n hasExpandedDescendants() {\n let sibling = this.nextItem;\n return !!sibling && sibling.depth > this.depth;\n }\n\n /**\n * Recursively traverse the node and its descendants.\n *\n * @callback {Item}\n */\n traverse(callback) {\n // capture descendants before traversal in case of side-effects\n // specifically, setting depth affects calculation\n const expanded = this.#expandedDescendants;\n\n callback(this);\n this.#traverseCollapsed(callback);\n expanded.forEach((item) => item.#traverseCollapsed(callback));\n }\n\n /**\n * Recursively traverse the node's collapsed descendants, if any.\n *\n * @callback {Item}\n */\n #traverseCollapsed(callback) {\n if (!this.hasCollapsedDescendants()) return;\n\n this.#collapsedDescendants.forEach((item) => {\n callback(item);\n item.#traverseCollapsed(callback);\n });\n }\n\n /**\n * Collapses visible (logical) children into this element's hidden children\n * list, creating it if it doesn't already exist.\n */\n collapse() {\n let listElement = this.#childrenListElement;\n\n if (!listElement) listElement = createChildrenList(this.node);\n\n this.#expandedDescendants.forEach((child) =>\n listElement.appendChild(child.node)\n );\n }\n\n /**\n * Moves any collapsed children back into the parent menu.\n */\n expand() {\n if (!this.hasCollapsedDescendants()) return;\n\n Array.from(this.#childrenListElement.children)\n .reverse()\n .forEach((node) => {\n this.node.insertAdjacentElement(\"afterend\", node);\n });\n }\n\n /**\n * Sets the state of a given rule on the target node.\n *\n * @param rule {String}\n * @param deny {boolean}\n */\n toggleRule(rule, deny = false) {\n if (this.node.dataset.hasOwnProperty(rule) && !deny) {\n delete this.node.dataset[rule];\n }\n if (!this.node.dataset.hasOwnProperty(rule) && deny) {\n this.node.dataset[rule] = \"\";\n }\n\n if (rule === \"denyDrag\") {\n if (!this.node.hasAttribute(\"draggable\") && !deny) {\n this.node.setAttribute(\"draggable\", \"true\");\n }\n if (this.node.hasAttribute(\"draggable\") && deny) {\n this.node.removeAttribute(\"draggable\");\n }\n }\n }\n\n /**\n * Detects turbo item changes by comparing the dataset id with the input\n */\n hasItemIdChanged() {\n return !(this.#itemIdInput.value === this.itemId);\n }\n\n /**\n * Updates inputs, in case they don't match the data values, e.g., when the\n * nested inputs have been hot-swapped by turbo with data from the server.\n *\n * Updates itemId from input as that is the canonical source.\n */\n updateAfterChange() {\n this.itemId = this.#itemIdInput.value;\n this.#indexInput.value = this.index;\n this.#depthInput.value = this.depth;\n }\n\n /**\n * Finds the dom container for storing collapsed (hidden) children, if present.\n *\n * @returns {Element} ol[data-navigation-children]\n */\n get #childrenListElement() {\n return this.node.querySelector(`:scope > [data-navigation-children]`);\n }\n\n /**\n * @returns {Item[]} all items that follow this element that have a greater depth.\n */\n get #expandedDescendants() {\n const descendants = [];\n\n let sibling = this.nextItem;\n while (sibling && sibling.depth > this.depth) {\n descendants.push(sibling);\n sibling = sibling.nextItem;\n }\n\n return descendants;\n }\n\n /**\n * @returns {Item[]} all items directly contained inside this element's hidden children element.\n */\n get #collapsedDescendants() {\n if (!this.hasCollapsedDescendants()) return [];\n\n return Array.from(this.#childrenListElement.children).map(\n (node) => new Item(node)\n );\n }\n}\n\n/**\n * Finds or creates a dom container for storing collapsed (hidden) children.\n *\n * @param node {Element} li[data-navigation-index]\n * @returns {Element} ol[data-navigation-children]\n */\nfunction createChildrenList(node) {\n const childrenList = document.createElement(\"ol\");\n childrenList.setAttribute(\"class\", \"hidden\");\n\n childrenList.dataset[`navigationChildren`] = \"\";\n\n node.appendChild(childrenList);\n\n return childrenList;\n}\n","import Item from \"./item\";\n\n/**\n * @param nodes {NodeList}\n * @returns {Item[]}\n */\nfunction createItemList(nodes) {\n return Array.from(nodes).map((node) => new Item(node));\n}\n\nexport default class Menu {\n /**\n * @param node {Element} navigation editor list\n */\n constructor(node) {\n this.node = node;\n }\n\n /**\n * @return {Item[]} an ordered list of all items in the menu\n */\n get items() {\n return createItemList(\n this.node.querySelectorAll(\"[data-navigation-index]\")\n );\n }\n\n /**\n * @return {String} a serialized description of the structure of the menu\n */\n get state() {\n const inputs = this.node.querySelectorAll(\"li input[type=hidden]\");\n return Array.from(inputs)\n .map((e) => e.value)\n .join(\"/\");\n }\n\n /**\n * Set the index of items based on their current position.\n */\n reindex() {\n this.items.map((item, index) => (item.index = index));\n }\n\n /**\n * Resets the order of items to their defined index.\n * Useful after an aborted drag.\n */\n reset() {\n this.items.sort(Item.comparator).forEach((item) => {\n this.node.appendChild(item.node);\n });\n }\n}\n","export default class RulesEngine {\n static rules = [\n \"denyDeNest\",\n \"denyNest\",\n \"denyCollapse\",\n \"denyExpand\",\n \"denyRemove\",\n \"denyDrag\",\n \"denyEdit\",\n ];\n\n constructor(maxDepth = null, debug = false) {\n this.maxDepth = maxDepth;\n if (debug) {\n this.debug = (...args) => console.log(...args);\n } else {\n this.debug = () => {};\n }\n }\n\n /**\n * Enforce structural rules to ensure that the given item is currently in a\n * valid state.\n *\n * @param {Item} item\n */\n normalize(item) {\n // structural rules enforce a valid tree structure\n this.firstItemDepthZero(item);\n this.depthMustBeSet(item);\n this.itemCannotHaveInvalidDepth(item);\n this.itemCannotExceedDepthLimit(item);\n this.parentMustBeLayout(item);\n this.parentCannotHaveExpandedAndCollapsedChildren(item);\n }\n\n /**\n * Apply rules to the given item to determine what operations are permitted.\n *\n * @param {Item} item\n */\n update(item) {\n this.rules = {};\n\n // behavioural rules define what the user is allowed to do\n this.parentsCannotDeNest(item);\n this.rootsCannotDeNest(item);\n this.nestingNeedsParent(item);\n this.nestingCannotExceedMaxDepth(item);\n this.leavesCannotCollapse(item);\n this.needHiddenItemsToExpand(item);\n this.parentsCannotBeDeleted(item);\n this.parentsCannotBeDragged(item);\n\n RulesEngine.rules.forEach((rule) => {\n item.toggleRule(rule, !!this.rules[rule]);\n });\n }\n\n /**\n * First item can't have a parent, so its depth should always be 0\n */\n firstItemDepthZero(item) {\n if (item.index === 0 && item.depth !== 0) {\n this.debug(`enforce depth on item ${item.index}: ${item.depth} => 0`);\n\n item.depth = 0;\n }\n }\n\n /**\n * Every item should have a non-negative depth set.\n *\n * @param {Item} item\n */\n depthMustBeSet(item) {\n if (isNaN(item.depth) || item.depth < 0) {\n this.debug(`unset depth on item ${item.index}: => 0`);\n\n item.depth = 0;\n }\n }\n\n /**\n * Depth must increase stepwise.\n *\n * @param {Item} item\n */\n itemCannotHaveInvalidDepth(item) {\n const previous = item.previousItem;\n if (previous && previous.depth < item.depth - 1) {\n this.debug(\n `invalid depth on item ${item.index}: ${item.depth} => ${\n previous.depth + 1\n }`\n );\n\n item.depth = previous.depth + 1;\n }\n }\n\n /**\n * Depth must not exceed menu's depth limit.\n *\n * @param {Item} item\n */\n itemCannotExceedDepthLimit(item) {\n if (this.maxDepth > 0 && this.maxDepth <= item.depth) {\n // Note: this change can cause an issue where the previous item is treated\n // like a parent even though it no longer has children. This is because\n // items are processed in order. This issue does not seem worth solving\n // as it only occurs if the max depth is altered. The issue can be worked\n // around by saving the menu.\n item.depth = this.maxDepth - 1;\n }\n }\n\n /**\n * Parent item, if any, must be a layout.\n *\n * @param {Item} item\n */\n parentMustBeLayout(item) {\n // if we're the first child, make sure our parent is a layout\n // if we're a sibling, we know the previous item is valid so we must be too\n const previous = item.previousItem;\n if (previous && previous.depth < item.depth && !previous.isLayout) {\n this.debug(\n `invalid parent for item ${item.index}: ${item.depth} => ${previous.depth}`\n );\n\n item.depth = previous.depth;\n }\n }\n\n /**\n * If a parent has expanded and collapsed children, expand.\n *\n * @param {Item} item\n */\n parentCannotHaveExpandedAndCollapsedChildren(item) {\n if (item.hasCollapsedDescendants() && item.hasExpandedDescendants()) {\n this.debug(`expanding collapsed children of item ${item.index}`);\n\n item.expand();\n }\n }\n\n /**\n * De-nesting an item would create a gap of 2 between itself and its children\n *\n * @param {Item} item\n */\n parentsCannotDeNest(item) {\n if (item.hasExpandedDescendants()) this.#deny(\"denyDeNest\");\n }\n\n /**\n * Item depth can't go below 0.\n *\n * @param {Item} item\n */\n rootsCannotDeNest(item) {\n if (item.depth === 0) this.#deny(\"denyDeNest\");\n }\n\n /**\n * If an item doesn't have children it can't be collapsed.\n *\n * @param {Item} item\n */\n leavesCannotCollapse(item) {\n if (!item.hasExpandedDescendants()) this.#deny(\"denyCollapse\");\n }\n\n /**\n * If an item doesn't have any hidden descendants then it can't be expanded.\n *\n * @param {Item} item\n */\n needHiddenItemsToExpand(item) {\n if (!item.hasCollapsedDescendants()) this.#deny(\"denyExpand\");\n }\n\n /**\n * An item can't be nested (indented) if it doesn't have a valid parent.\n *\n * @param {Item} item\n */\n nestingNeedsParent(item) {\n const previous = item.previousItem;\n // no previous, so cannot nest\n if (!previous) this.#deny(\"denyNest\");\n // previous is too shallow, nesting would increase depth too much\n else if (previous.depth < item.depth) this.#deny(\"denyNest\");\n // new parent is not a layout\n else if (previous.depth === item.depth && !previous.isLayout)\n this.#deny(\"denyNest\");\n }\n\n /**\n * An item can't be nested (indented) if doing so would exceed the max depth.\n *\n * @param {Item} item\n */\n nestingCannotExceedMaxDepth(item) {\n if (this.maxDepth > 0 && this.maxDepth <= item.depth + 1) {\n this.#deny(\"denyNest\");\n }\n }\n\n /**\n * An item can't be deleted if it has visible children.\n *\n * @param {Item} item\n */\n parentsCannotBeDeleted(item) {\n if (!item.itemId || item.hasExpandedDescendants()) this.#deny(\"denyRemove\");\n }\n\n /**\n * Items cannot be dragged if they have visible children.\n *\n * @param {Item} item\n */\n parentsCannotBeDragged(item) {\n if (item.hasExpandedDescendants()) this.#deny(\"denyDrag\");\n }\n\n /**\n * Record a deny.\n *\n * @param rule {String}\n */\n #deny(rule) {\n this.rules[rule] = true;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nimport Item from \"./item\";\nimport Menu from \"./menu\";\nimport RulesEngine from \"./rules-engine\";\n\nexport default class MenuController extends Controller {\n static targets = [\"menu\"];\n static values = {\n maxDepth: Number,\n };\n\n connect() {\n this.state = this.menu.state;\n\n this.reindex();\n }\n\n get menu() {\n return new Menu(this.menuTarget);\n }\n\n reindex() {\n this.menu.reindex();\n this.#update();\n }\n\n reset() {\n this.menu.reset();\n }\n\n drop(event) {\n this.menu.reindex(); // set indexes before calculating previous\n\n const item = getEventItem(event);\n const previous = item.previousItem;\n\n let delta = 0;\n if (previous === undefined) {\n // if previous does not exist, set depth to 0\n delta = -item.depth;\n } else if (item.nextItem && item.nextItem.depth > previous.depth) {\n // if next is a child of previous, make item a child of previous\n delta = previous.depth - item.depth + 1;\n } else {\n // otherwise, make item a sibling of previous\n delta = previous.depth - item.depth;\n }\n\n item.traverse((child) => {\n child.depth += delta;\n });\n\n this.#update();\n event.preventDefault();\n }\n\n remove(event) {\n const item = getEventItem(event);\n\n item.node.remove();\n\n this.#update();\n event.preventDefault();\n }\n\n nest(event) {\n const item = getEventItem(event);\n\n item.traverse((child) => {\n child.depth += 1;\n });\n\n this.#update();\n event.preventDefault();\n }\n\n deNest(event) {\n const item = getEventItem(event);\n\n item.traverse((child) => {\n child.depth -= 1;\n });\n\n this.#update();\n event.preventDefault();\n }\n\n collapse(event) {\n const item = getEventItem(event);\n\n item.collapse();\n\n this.#update();\n event.preventDefault();\n }\n\n expand(event) {\n const item = getEventItem(event);\n\n item.expand();\n\n this.#update();\n event.preventDefault();\n }\n\n /**\n * Re-apply rules to items to enable/disable appropriate actions.\n */\n #update() {\n // debounce requests to ensure that we only update once per tick\n this.updateRequested = true;\n setTimeout(() => {\n if (!this.updateRequested) return;\n\n this.updateRequested = false;\n const engine = new RulesEngine(this.maxDepthValue);\n this.menu.items.forEach((item) => engine.normalize(item));\n this.menu.items.forEach((item) => engine.update(item));\n\n this.#notifyChange();\n }, 0);\n }\n\n #notifyChange() {\n this.dispatch(\"change\", {\n bubbles: true,\n prefix: \"navigation\",\n detail: { dirty: this.#isDirty() },\n });\n }\n\n #isDirty() {\n return this.menu.state !== this.state;\n }\n}\n\nfunction getEventItem(event) {\n return new Item(event.target.closest(\"[data-navigation-item]\"));\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class ListController extends Controller {\n dragstart(event) {\n if (this.element !== event.target.parentElement) return;\n\n const target = event.target;\n event.dataTransfer.effectAllowed = \"move\";\n\n // update element style after drag has begun\n setTimeout(() => (target.dataset.dragging = \"\"));\n }\n\n dragover(event) {\n const item = this.dragItem();\n if (!item) return;\n\n swap(this.dropTarget(event.target), item);\n\n event.preventDefault();\n return true;\n }\n\n dragenter(event) {\n event.preventDefault();\n\n if (event.dataTransfer.effectAllowed === \"copy\" && !this.dragItem()) {\n const item = document.createElement(\"li\");\n item.dataset.dragging = \"\";\n item.dataset.newItem = \"\";\n this.element.prepend(item);\n }\n }\n\n dragleave(event) {\n const item = this.dragItem();\n const related = this.dropTarget(event.relatedTarget);\n\n // ignore if item is not set or we're moving into a valid drop target\n if (!item || related) return;\n\n // remove item if it's a new item\n if (item.dataset.hasOwnProperty(\"newItem\")) {\n item.remove();\n }\n }\n\n drop(event) {\n let item = this.dragItem();\n\n if (!item) return;\n\n event.preventDefault();\n delete item.dataset.dragging;\n swap(this.dropTarget(event.target), item);\n\n if (item.dataset.hasOwnProperty(\"newItem\")) {\n const placeholder = item;\n const template = document.createElement(\"template\");\n template.innerHTML = event.dataTransfer.getData(\"text/html\");\n item = template.content.querySelector(\"li\");\n\n this.element.replaceChild(item, placeholder);\n setTimeout(() =>\n item.querySelector(\"[role='button'][value='edit']\").click()\n );\n }\n\n this.dispatch(\"drop\", {\n target: item,\n bubbles: true,\n prefix: \"navigation\",\n });\n }\n\n dragend() {\n const item = this.dragItem();\n if (!item) return;\n\n delete item.dataset.dragging;\n this.reset();\n }\n\n dragItem() {\n return this.element.querySelector(\"[data-dragging]\");\n }\n\n dropTarget(e) {\n return (\n e.closest(\"[data-controller='navigation--editor--list'] > *\") ||\n e.closest(\"[data-controller='navigation--editor--list']\")\n );\n }\n\n reindex() {\n this.dispatch(\"reindex\", { bubbles: true, prefix: \"navigation\" });\n }\n\n reset() {\n this.dispatch(\"reset\", { bubbles: true, prefix: \"navigation\" });\n }\n}\n\nfunction swap(target, item) {\n if (!target) return;\n if (target === item) return;\n\n if (target.nodeName === \"LI\") {\n const positionComparison = target.compareDocumentPosition(item);\n if (positionComparison & Node.DOCUMENT_POSITION_FOLLOWING) {\n target.insertAdjacentElement(\"beforebegin\", item);\n } else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {\n target.insertAdjacentElement(\"afterend\", item);\n }\n }\n\n if (target.nodeName === \"OL\") {\n target.appendChild(item);\n }\n}\n","import MenuController from \"./editor/menu_controller\";\nimport ItemController from \"./editor/item_controller\";\nimport ListController from \"./editor/list_controller\";\nimport NewItemController from \"./editor/new_item_controller\";\nimport StatusBarController from \"./editor/status_bar_controller\";\n\nconst Definitions = [\n {\n identifier: \"navigation--editor--menu\",\n controllerConstructor: MenuController,\n },\n {\n identifier: \"navigation--editor--item\",\n controllerConstructor: ItemController,\n },\n {\n identifier: \"navigation--editor--list\",\n controllerConstructor: ListController,\n },\n {\n identifier: \"navigation--editor--new-item\",\n controllerConstructor: NewItemController,\n },\n {\n identifier: \"navigation--editor--status-bar\",\n controllerConstructor: StatusBarController,\n },\n];\n\nexport { Definitions as default };\n","import { Controller } from \"@hotwired/stimulus\";\nimport Item from \"./item\";\n\nexport default class ItemController extends Controller {\n get item() {\n return new Item(this.li);\n }\n\n get ol() {\n return this.element.closest(\"ol\");\n }\n\n get li() {\n return this.element.closest(\"li\");\n }\n\n connect() {\n if (this.element.dataset.hasOwnProperty(\"delete\")) {\n this.remove();\n }\n // if index is not already set, re-index will set it\n else if (!(this.item.index >= 0)) {\n this.reindex();\n }\n // if item has been replaced via turbo, re-index will run the rules engine\n // update our depth and index with values from the li's data attributes\n else if (this.item.hasItemIdChanged()) {\n this.item.updateAfterChange();\n this.reindex();\n }\n }\n\n remove() {\n // capture ol\n const ol = this.ol;\n // remove self from dom\n this.li.remove();\n // reindex ol\n this.reindex();\n }\n\n reindex() {\n this.dispatch(\"reindex\", { bubbles: true, prefix: \"navigation\" });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class NewItemController extends Controller {\n static targets = [\"template\"];\n\n dragstart(event) {\n if (this.element !== event.target) return;\n\n event.dataTransfer.setData(\"text/html\", this.templateTarget.innerHTML);\n event.dataTransfer.effectAllowed = \"copy\";\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\n\nexport default class StatusBarController extends Controller {\n connect() {\n // cache the version's state in the controller on connect\n this.versionState = this.element.dataset.state;\n }\n\n change(e) {\n if (e.detail && e.detail.hasOwnProperty(\"dirty\")) {\n this.update(e.detail);\n }\n }\n\n update({ dirty }) {\n if (dirty) {\n this.element.dataset.state = \"dirty\";\n } else {\n this.element.dataset.state = this.versionState;\n }\n }\n}\n"],"names":["Item","comparator","a","b","index","constructor","node","this","itemId","dataset","itemIdInput","querySelector","id","value","depth","parseInt","depthInput","indexInput","isLayout","hasAttribute","previousItem","sibling","previousElementSibling","nextItem","nextElementSibling","hasCollapsedDescendants","childrenList","childrenListElement","children","length","hasExpandedDescendants","traverse","callback","expanded","expandedDescendants","traverseCollapsed","forEach","item","collapsedDescendants","collapse","listElement","document","createElement","setAttribute","appendChild","createChildrenList","child","expand","Array","from","reverse","insertAdjacentElement","toggleRule","rule","deny","hasOwnProperty","removeAttribute","hasItemIdChanged","updateAfterChange","descendants","push","map","Menu","items","nodes","querySelectorAll","state","inputs","e","join","reindex","reset","sort","RulesEngine","static","maxDepth","debug","args","console","log","normalize","firstItemDepthZero","depthMustBeSet","itemCannotHaveInvalidDepth","itemCannotExceedDepthLimit","parentMustBeLayout","parentCannotHaveExpandedAndCollapsedChildren","update","rules","parentsCannotDeNest","rootsCannotDeNest","nestingNeedsParent","nestingCannotExceedMaxDepth","leavesCannotCollapse","needHiddenItemsToExpand","parentsCannotBeDeleted","parentsCannotBeDragged","isNaN","previous","getEventItem","event","target","closest","swap","nodeName","positionComparison","compareDocumentPosition","Node","DOCUMENT_POSITION_FOLLOWING","DOCUMENT_POSITION_PRECEDING","Definitions","identifier","controllerConstructor","Controller","Number","connect","menu","menuTarget","drop","delta","undefined","preventDefault","remove","nest","deNest","updateRequested","setTimeout","engine","maxDepthValue","notifyChange","dispatch","bubbles","prefix","detail","dirty","isDirty","li","ol","element","dragstart","parentElement","dataTransfer","effectAllowed","dragging","dragover","dragItem","dropTarget","dragenter","newItem","prepend","dragleave","related","relatedTarget","placeholder","template","innerHTML","getData","content","replaceChild","click","dragend","setData","templateTarget","versionState","change"],"mappings":"gDAAe,MAAMA,EAQnB,iBAAOC,CAAWC,EAAGC,GACnB,OAAOD,EAAEE,MAAQD,EAAEC,KACpB,CAKD,WAAAC,CAAYC,GACVC,KAAKD,KAAOA,CACb,CAKD,UAAIE,GACF,OAAOD,KAAKD,KAAKG,QAA0B,gBAC5C,CAED,KAAIC,GACF,OAAOH,KAAKD,KAAKK,cAAc,sBAChC,CAKD,UAAIH,CAAOI,GACLL,KAAKC,SAAWI,IAEpBL,KAAKD,KAAKG,QAA0B,iBAAI,GAAGG,IAC3CL,MAAKG,EAAaG,MAAQ,GAAGD,IAC9B,CAKD,SAAIE,GACF,OAAOC,SAASR,KAAKD,KAAKG,QAAyB,kBAAM,CAC1D,CAED,KAAIO,GACF,OAAOT,KAAKD,KAAKK,cAAc,yBAChC,CAKD,SAAIG,CAAMA,GACJP,KAAKO,QAAUA,IAEnBP,KAAKD,KAAKG,QAAyB,gBAAI,GAAGK,IAC1CP,MAAKS,EAAYH,MAAQ,GAAGC,IAC7B,CAKD,SAAIV,GACF,OAAOW,SAASR,KAAKD,KAAKG,QAAyB,gBACpD,CAED,KAAIQ,GACF,OAAOV,KAAKD,KAAKK,cAAc,yBAChC,CAKD,SAAIP,CAAMA,GACJG,KAAKH,QAAUA,IAEnBG,KAAKD,KAAKG,QAAyB,gBAAI,GAAGL,IAC1CG,MAAKU,EAAYJ,MAAQ,GAAGT,IAC7B,CAKD,YAAIc,GACF,OAAOX,KAAKD,KAAKa,aAAa,sBAC/B,CAKD,gBAAIC,GACF,IAAIC,EAAUd,KAAKD,KAAKgB,uBACxB,GAAID,EAAS,OAAO,IAAIrB,EAAKqB,EAC9B,CAKD,YAAIE,GACF,IAAIF,EAAUd,KAAKD,KAAKkB,mBACxB,GAAIH,EAAS,OAAO,IAAIrB,EAAKqB,EAC9B,CAKD,uBAAAI,GACE,IAAIC,EAAenB,MAAKoB,EACxB,QAASD,GAAgBA,EAAaE,SAASC,OAAS,CACzD,CAKD,sBAAAC,GACE,IAAIT,EAAUd,KAAKgB,SACnB,QAASF,GAAWA,EAAQP,MAAQP,KAAKO,KAC1C,CAOD,QAAAiB,CAASC,GAGP,MAAMC,EAAW1B,MAAK2B,EAEtBF,EAASzB,MACTA,MAAK4B,EAAmBH,GACxBC,EAASG,SAASC,GAASA,GAAKF,EAAmBH,IACpD,CAOD,EAAAG,CAAmBH,GACZzB,KAAKkB,2BAEVlB,MAAK+B,EAAsBF,SAASC,IAClCL,EAASK,GACTA,GAAKF,EAAmBH,EAAS,GAEpC,CAMD,QAAAO,GACE,IAAIC,EAAcjC,MAAKoB,EAElBa,IAAaA,EAyGtB,SAA4BlC,GAC1B,MAAMoB,EAAee,SAASC,cAAc,MAO5C,OANAhB,EAAaiB,aAAa,QAAS,UAEnCjB,EAAajB,QAA4B,mBAAI,GAE7CH,EAAKsC,YAAYlB,GAEVA,CACT,CAlHoCmB,CAAmBtC,KAAKD,OAExDC,MAAK2B,EAAqBE,SAASU,GACjCN,EAAYI,YAAYE,EAAMxC,OAEjC,CAKD,MAAAyC,GACOxC,KAAKkB,2BAEVuB,MAAMC,KAAK1C,MAAKoB,EAAqBC,UAClCsB,UACAd,SAAS9B,IACRC,KAAKD,KAAK6C,sBAAsB,WAAY7C,EAAK,GAEtD,CAQD,UAAA8C,CAAWC,EAAMC,GAAO,GAClB/C,KAAKD,KAAKG,QAAQ8C,eAAeF,KAAUC,UACtC/C,KAAKD,KAAKG,QAAQ4C,IAEtB9C,KAAKD,KAAKG,QAAQ8C,eAAeF,IAASC,IAC7C/C,KAAKD,KAAKG,QAAQ4C,GAAQ,IAGf,aAATA,IACG9C,KAAKD,KAAKa,aAAa,cAAiBmC,GAC3C/C,KAAKD,KAAKqC,aAAa,YAAa,QAElCpC,KAAKD,KAAKa,aAAa,cAAgBmC,GACzC/C,KAAKD,KAAKkD,gBAAgB,aAG/B,CAKD,gBAAAC,GACE,QAASlD,MAAKG,EAAaG,QAAUN,KAAKC,OAC3C,CAQD,iBAAAkD,GACEnD,KAAKC,OAASD,MAAKG,EAAaG,MAChCN,MAAKU,EAAYJ,MAAQN,KAAKH,MAC9BG,MAAKS,EAAYH,MAAQN,KAAKO,KAC/B,CAOD,KAAIa,GACF,OAAOpB,KAAKD,KAAKK,cAAc,sCAChC,CAKD,KAAIuB,GACF,MAAMyB,EAAc,GAEpB,IAAItC,EAAUd,KAAKgB,SACnB,KAAOF,GAAWA,EAAQP,MAAQP,KAAKO,OACrC6C,EAAYC,KAAKvC,GACjBA,EAAUA,EAAQE,SAGpB,OAAOoC,CACR,CAKD,KAAIrB,GACF,OAAK/B,KAAKkB,0BAEHuB,MAAMC,KAAK1C,MAAKoB,EAAqBC,UAAUiC,KACnDvD,GAAS,IAAIN,EAAKM,KAHuB,EAK7C,ECnPY,MAAMwD,EAInB,WAAAzD,CAAYC,GACVC,KAAKD,KAAOA,CACb,CAKD,SAAIyD,GACF,OAhBoBC,EAiBlBzD,KAAKD,KAAK2D,iBAAiB,2BAhBxBjB,MAAMC,KAAKe,GAAOH,KAAKvD,GAAS,IAAIN,EAAKM,KADlD,IAAwB0D,CAmBrB,CAKD,SAAIE,GACF,MAAMC,EAAS5D,KAAKD,KAAK2D,iBAAiB,yBAC1C,OAAOjB,MAAMC,KAAKkB,GACfN,KAAKO,GAAMA,EAAEvD,QACbwD,KAAK,IACT,CAKD,OAAAC,GACE/D,KAAKwD,MAAMF,KAAI,CAACxB,EAAMjC,IAAWiC,EAAKjC,MAAQA,GAC/C,CAMD,KAAAmE,GACEhE,KAAKwD,MAAMS,KAAKxE,EAAKC,YAAYmC,SAASC,IACxC9B,KAAKD,KAAKsC,YAAYP,EAAK/B,KAAK,GAEnC,ECpDY,MAAMmE,EACnBC,aAAe,CACb,aACA,WACA,eACA,aACA,aACA,WACA,YAGF,WAAArE,CAAYsE,EAAW,KAAMC,GAAQ,GACnCrE,KAAKoE,SAAWA,EAEdpE,KAAKqE,MADHA,EACW,IAAIC,IAASC,QAAQC,OAAOF,GAE5B,MAEhB,CAQD,SAAAG,CAAU3C,GAER9B,KAAK0E,mBAAmB5C,GACxB9B,KAAK2E,eAAe7C,GACpB9B,KAAK4E,2BAA2B9C,GAChC9B,KAAK6E,2BAA2B/C,GAChC9B,KAAK8E,mBAAmBhD,GACxB9B,KAAK+E,6CAA6CjD,EACnD,CAOD,MAAAkD,CAAOlD,GACL9B,KAAKiF,MAAQ,GAGbjF,KAAKkF,oBAAoBpD,GACzB9B,KAAKmF,kBAAkBrD,GACvB9B,KAAKoF,mBAAmBtD,GACxB9B,KAAKqF,4BAA4BvD,GACjC9B,KAAKsF,qBAAqBxD,GAC1B9B,KAAKuF,wBAAwBzD,GAC7B9B,KAAKwF,uBAAuB1D,GAC5B9B,KAAKyF,uBAAuB3D,GAE5BoC,EAAYe,MAAMpD,SAASiB,IACzBhB,EAAKe,WAAWC,IAAQ9C,KAAKiF,MAAMnC,GAAM,GAE5C,CAKD,kBAAA4B,CAAmB5C,GACE,IAAfA,EAAKjC,OAA8B,IAAfiC,EAAKvB,QAC3BP,KAAKqE,MAAM,yBAAyBvC,EAAKjC,UAAUiC,EAAKvB,cAExDuB,EAAKvB,MAAQ,EAEhB,CAOD,cAAAoE,CAAe7C,IACT4D,MAAM5D,EAAKvB,QAAUuB,EAAKvB,MAAQ,KACpCP,KAAKqE,MAAM,uBAAuBvC,EAAKjC,eAEvCiC,EAAKvB,MAAQ,EAEhB,CAOD,0BAAAqE,CAA2B9C,GACzB,MAAM6D,EAAW7D,EAAKjB,aAClB8E,GAAYA,EAASpF,MAAQuB,EAAKvB,MAAQ,IAC5CP,KAAKqE,MACH,yBAAyBvC,EAAKjC,UAAUiC,EAAKvB,YAC3CoF,EAASpF,MAAQ,KAIrBuB,EAAKvB,MAAQoF,EAASpF,MAAQ,EAEjC,CAOD,0BAAAsE,CAA2B/C,GACrB9B,KAAKoE,SAAW,GAAKpE,KAAKoE,UAAYtC,EAAKvB,QAM7CuB,EAAKvB,MAAQP,KAAKoE,SAAW,EAEhC,CAOD,kBAAAU,CAAmBhD,GAGjB,MAAM6D,EAAW7D,EAAKjB,aAClB8E,GAAYA,EAASpF,MAAQuB,EAAKvB,QAAUoF,EAAShF,WACvDX,KAAKqE,MACH,2BAA2BvC,EAAKjC,UAAUiC,EAAKvB,YAAYoF,EAASpF,SAGtEuB,EAAKvB,MAAQoF,EAASpF,MAEzB,CAOD,4CAAAwE,CAA6CjD,GACvCA,EAAKZ,2BAA6BY,EAAKP,2BACzCvB,KAAKqE,MAAM,wCAAwCvC,EAAKjC,SAExDiC,EAAKU,SAER,CAOD,mBAAA0C,CAAoBpD,GACdA,EAAKP,0BAA0BvB,MAAK+C,EAAM,aAC/C,CAOD,iBAAAoC,CAAkBrD,GACG,IAAfA,EAAKvB,OAAaP,MAAK+C,EAAM,aAClC,CAOD,oBAAAuC,CAAqBxD,GACdA,EAAKP,0BAA0BvB,MAAK+C,EAAM,eAChD,CAOD,uBAAAwC,CAAwBzD,GACjBA,EAAKZ,2BAA2BlB,MAAK+C,EAAM,aACjD,CAOD,kBAAAqC,CAAmBtD,GACjB,MAAM6D,EAAW7D,EAAKjB,aAEjB8E,EAEIA,EAASpF,MAAQuB,EAAKvB,MAAOP,MAAK+C,EAAM,YAExC4C,EAASpF,QAAUuB,EAAKvB,OAAUoF,EAAShF,UAClDX,MAAK+C,EAAM,YALE/C,MAAK+C,EAAM,WAM3B,CAOD,2BAAAsC,CAA4BvD,GACtB9B,KAAKoE,SAAW,GAAKpE,KAAKoE,UAAYtC,EAAKvB,MAAQ,GACrDP,MAAK+C,EAAM,WAEd,CAOD,sBAAAyC,CAAuB1D,GAChBA,EAAK7B,SAAU6B,EAAKP,0BAA0BvB,MAAK+C,EAAM,aAC/D,CAOD,sBAAA0C,CAAuB3D,GACjBA,EAAKP,0BAA0BvB,MAAK+C,EAAM,WAC/C,CAOD,EAAAA,CAAMD,GACJ9C,KAAKiF,MAAMnC,IAAQ,CACpB,ECnGH,SAAS8C,EAAaC,GACpB,OAAO,IAAIpG,EAAKoG,EAAMC,OAAOC,QAAQ,0BACvC,CCpCA,SAASC,EAAKF,EAAQhE,GACpB,GAAKgE,GACDA,IAAWhE,EAAf,CAEA,GAAwB,OAApBgE,EAAOG,SAAmB,CAC5B,MAAMC,EAAqBJ,EAAOK,wBAAwBrE,GACtDoE,EAAqBE,KAAKC,4BAC5BP,EAAOlD,sBAAsB,cAAed,GACnCoE,EAAqBE,KAAKE,6BACnCR,EAAOlD,sBAAsB,WAAYd,EAE5C,CAEuB,OAApBgE,EAAOG,UACTH,EAAOzD,YAAYP,EAZO,CAc9B,CCjHK,MAACyE,EAAc,CAClB,CACEC,WAAY,2BACZC,sBFHW,cAA6BC,EAC1CvC,eAAiB,CAAC,QAClBA,cAAgB,CACdC,SAAUuC,QAGZ,OAAAC,GACE5G,KAAK2D,MAAQ3D,KAAK6G,KAAKlD,MAEvB3D,KAAK+D,SACN,CAED,QAAI8C,GACF,OAAO,IAAItD,EAAKvD,KAAK8G,WACtB,CAED,OAAA/C,GACE/D,KAAK6G,KAAK9C,UACV/D,MAAKgF,GACN,CAED,KAAAhB,GACEhE,KAAK6G,KAAK7C,OACX,CAED,IAAA+C,CAAKlB,GACH7F,KAAK6G,KAAK9C,UAEV,MAAMjC,EAAO8D,EAAaC,GACpBF,EAAW7D,EAAKjB,aAEtB,IAAImG,EAAQ,EAGVA,OAFeC,IAAbtB,GAEO7D,EAAKvB,MACLuB,EAAKd,UAAYc,EAAKd,SAAST,MAAQoF,EAASpF,MAEjDoF,EAASpF,MAAQuB,EAAKvB,MAAQ,EAG9BoF,EAASpF,MAAQuB,EAAKvB,MAGhCuB,EAAKN,UAAUe,IACbA,EAAMhC,OAASyG,CAAK,IAGtBhH,MAAKgF,IACLa,EAAMqB,gBACP,CAED,MAAAC,CAAOtB,GACQD,EAAaC,GAErB9F,KAAKoH,SAEVnH,MAAKgF,IACLa,EAAMqB,gBACP,CAED,IAAAE,CAAKvB,GACUD,EAAaC,GAErBrE,UAAUe,IACbA,EAAMhC,OAAS,CAAC,IAGlBP,MAAKgF,IACLa,EAAMqB,gBACP,CAED,MAAAG,CAAOxB,GACQD,EAAaC,GAErBrE,UAAUe,IACbA,EAAMhC,OAAS,CAAC,IAGlBP,MAAKgF,IACLa,EAAMqB,gBACP,CAED,QAAAlF,CAAS6D,GACMD,EAAaC,GAErB7D,WAELhC,MAAKgF,IACLa,EAAMqB,gBACP,CAED,MAAA1E,CAAOqD,GACQD,EAAaC,GAErBrD,SAELxC,MAAKgF,IACLa,EAAMqB,gBACP,CAKD,EAAAlC,GAEEhF,KAAKsH,iBAAkB,EACvBC,YAAW,KACT,IAAKvH,KAAKsH,gBAAiB,OAE3BtH,KAAKsH,iBAAkB,EACvB,MAAME,EAAS,IAAItD,EAAYlE,KAAKyH,eACpCzH,KAAK6G,KAAKrD,MAAM3B,SAASC,GAAS0F,EAAO/C,UAAU3C,KACnD9B,KAAK6G,KAAKrD,MAAM3B,SAASC,GAAS0F,EAAOxC,OAAOlD,KAEhD9B,MAAK0H,GAAe,GACnB,EACJ,CAED,EAAAA,GACE1H,KAAK2H,SAAS,SAAU,CACtBC,SAAS,EACTC,OAAQ,aACRC,OAAQ,CAAEC,MAAO/H,MAAKgI,MAEzB,CAED,EAAAA,GACE,OAAOhI,KAAK6G,KAAKlD,QAAU3D,KAAK2D,KACjC,IE3HD,CACE6C,WAAY,2BACZC,sBCVW,cAA6BC,EAC1C,QAAI5E,GACF,OAAO,IAAIrC,EAAKO,KAAKiI,GACtB,CAED,MAAIC,GACF,OAAOlI,KAAKmI,QAAQpC,QAAQ,KAC7B,CAED,MAAIkC,GACF,OAAOjI,KAAKmI,QAAQpC,QAAQ,KAC7B,CAED,OAAAa,GACM5G,KAAKmI,QAAQjI,QAAQ8C,eAAe,UACtChD,KAAKmH,SAGInH,KAAK8B,KAAKjC,OAAS,EAKrBG,KAAK8B,KAAKoB,qBACjBlD,KAAK8B,KAAKqB,oBACVnD,KAAK+D,WANL/D,KAAK+D,SAQR,CAED,MAAAoD,GAEanH,KAAKkI,GAEhBlI,KAAKiI,GAAGd,SAERnH,KAAK+D,SACN,CAED,OAAAA,GACE/D,KAAK2H,SAAS,UAAW,CAAEC,SAAS,EAAMC,OAAQ,cACnD,ID5BD,CACErB,WAAY,2BACZC,sBDfW,cAA6BC,EAC1C,SAAA0B,CAAUvC,GACR,GAAI7F,KAAKmI,UAAYtC,EAAMC,OAAOuC,cAAe,OAEjD,MAAMvC,EAASD,EAAMC,OACrBD,EAAMyC,aAAaC,cAAgB,OAGnChB,YAAW,IAAOzB,EAAO5F,QAAQsI,SAAW,IAC7C,CAED,QAAAC,CAAS5C,GACP,MAAM/D,EAAO9B,KAAK0I,WAClB,GAAK5G,EAKL,OAHAkE,EAAKhG,KAAK2I,WAAW9C,EAAMC,QAAShE,GAEpC+D,EAAMqB,kBACC,CACR,CAED,SAAA0B,CAAU/C,GAGR,GAFAA,EAAMqB,iBAEmC,SAArCrB,EAAMyC,aAAaC,gBAA6BvI,KAAK0I,WAAY,CACnE,MAAM5G,EAAOI,SAASC,cAAc,MACpCL,EAAK5B,QAAQsI,SAAW,GACxB1G,EAAK5B,QAAQ2I,QAAU,GACvB7I,KAAKmI,QAAQW,QAAQhH,EACtB,CACF,CAED,SAAAiH,CAAUlD,GACR,MAAM/D,EAAO9B,KAAK0I,WACZM,EAAUhJ,KAAK2I,WAAW9C,EAAMoD,eAGjCnH,IAAQkH,GAGTlH,EAAK5B,QAAQ8C,eAAe,YAC9BlB,EAAKqF,QAER,CAED,IAAAJ,CAAKlB,GACH,IAAI/D,EAAO9B,KAAK0I,WAEhB,GAAK5G,EAAL,CAMA,GAJA+D,EAAMqB,wBACCpF,EAAK5B,QAAQsI,SACpBxC,EAAKhG,KAAK2I,WAAW9C,EAAMC,QAAShE,GAEhCA,EAAK5B,QAAQ8C,eAAe,WAAY,CAC1C,MAAMkG,EAAcpH,EACdqH,EAAWjH,SAASC,cAAc,YACxCgH,EAASC,UAAYvD,EAAMyC,aAAae,QAAQ,aAChDvH,EAAOqH,EAASG,QAAQlJ,cAAc,MAEtCJ,KAAKmI,QAAQoB,aAAazH,EAAMoH,GAChC3B,YAAW,IACTzF,EAAK1B,cAAc,iCAAiCoJ,SAEvD,CAEDxJ,KAAK2H,SAAS,OAAQ,CACpB7B,OAAQhE,EACR8F,SAAS,EACTC,OAAQ,cArBQ,CAuBnB,CAED,OAAA4B,GACE,MAAM3H,EAAO9B,KAAK0I,WACb5G,WAEEA,EAAK5B,QAAQsI,SACpBxI,KAAKgE,QACN,CAED,QAAA0E,GACE,OAAO1I,KAAKmI,QAAQ/H,cAAc,kBACnC,CAED,UAAAuI,CAAW9E,GACT,OACEA,EAAEkC,QAAQ,qDACVlC,EAAEkC,QAAQ,+CAEb,CAED,OAAAhC,GACE/D,KAAK2H,SAAS,UAAW,CAAEC,SAAS,EAAMC,OAAQ,cACnD,CAED,KAAA7D,GACEhE,KAAK2H,SAAS,QAAS,CAAEC,SAAS,EAAMC,OAAQ,cACjD,ICjFD,CACErB,WAAY,+BACZC,sBEnBW,cAAgCC,EAC7CvC,eAAiB,CAAC,YAElB,SAAAiE,CAAUvC,GACJ7F,KAAKmI,UAAYtC,EAAMC,SAE3BD,EAAMyC,aAAaoB,QAAQ,YAAa1J,KAAK2J,eAAeP,WAC5DvD,EAAMyC,aAAaC,cAAgB,OACpC,IFaD,CACE/B,WAAY,iCACZC,sBGvBW,cAAkCC,EAC/C,OAAAE,GAEE5G,KAAK4J,aAAe5J,KAAKmI,QAAQjI,QAAQyD,KAC1C,CAED,MAAAkG,CAAOhG,GACDA,EAAEiE,QAAUjE,EAAEiE,OAAO9E,eAAe,UACtChD,KAAKgF,OAAOnB,EAAEiE,OAEjB,CAED,MAAA9C,EAAO+C,MAAEA,IAEL/H,KAAKmI,QAAQjI,QAAQyD,MADnBoE,EAC2B,QAEA/H,KAAK4J,YAErC"}
@@ -1 +1 @@
1
- //= link_tree ../javascripts
1
+ //= link_tree ../builds
@@ -55,7 +55,7 @@ $status-dirty-color: #aaa !default;
55
55
  }
56
56
 
57
57
  [data-controller="navigation--editor--list"] {
58
- min-height: var(--row-height);
58
+ min-height: calc(var(--row-height) * 8);
59
59
 
60
60
  // tree items
61
61
  & > li {
@@ -111,7 +111,7 @@ $status-dirty-color: #aaa !default;
111
111
  [data-controller="navigation--editor--menu"] [role="rowheader"],
112
112
  [data-controller="navigation--editor--item"] {
113
113
  display: grid;
114
- grid-template-columns: 40% 2fr auto;
114
+ grid-template-columns: minmax(10rem, calc(100% - 12rem)) 1fr 10rem;
115
115
  padding: 0.25rem 0.5rem;
116
116
  gap: 1rem;
117
117
  align-items: center;
@@ -2,30 +2,40 @@
2
2
 
3
3
  .navigation--editor--new-items {
4
4
  display: grid;
5
- grid-template-columns: repeat(3, 1fr);
5
+ grid-template-columns: repeat(3, calc((100% - 1rem) / 3));
6
+ grid-auto-rows: minmax(5rem, auto);
6
7
  gap: 0.5rem;
7
8
 
8
9
  [role="listitem"] {
9
- display: flex;
10
- flex-direction: column;
11
- justify-content: center;
12
- align-items: center;
10
+ display: grid;
11
+ grid-template-rows: 1fr auto;
12
+ text-align: center;
13
13
  transform: translate3d(0, 0, 0);
14
14
  cursor: grab;
15
15
  background: white;
16
- box-shadow: rgb(0 0 0 / 25%) 0 1px 1px, rgb(0 0 0 / 31%) 0 0 2px;
17
- min-height: 5rem;
18
- padding: 1.5rem;
16
+ padding: 0.5rem;
17
+ white-space: nowrap;
18
+ border-radius: 0.5rem;
19
+
20
+ label {
21
+ font-weight: 400;
22
+ font-size: 0.8rem;
23
+ overflow: hidden;
24
+ text-overflow: ellipsis;
25
+ margin-bottom: 0;
26
+ }
19
27
 
20
28
  &:hover {
21
29
  box-shadow: rgb(0 0 0 / 25%) 0 1px 2px, rgb(0 0 0 / 31%) 0 0 5px;
22
30
  }
23
31
 
24
32
  &::before {
33
+ display: block;
25
34
  @extend %icon;
26
- width: 2.5rem;
27
- height: 2.5rem;
35
+ width: 2rem;
36
+ height: 2rem;
28
37
  position: unset;
38
+ margin: 0 auto;
29
39
  }
30
40
 
31
41
  &[data-item-type="heading"]:before {
@@ -41,3 +51,15 @@
41
51
  }
42
52
  }
43
53
  }
54
+
55
+ @media (max-width: 75rem) {
56
+ .navigation--editor--new-items {
57
+ grid-template-columns: repeat(2, calc((100% - 0.5rem) / 2));
58
+ }
59
+ }
60
+
61
+ @media (max-width: 60rem) {
62
+ .navigation--editor--new-items {
63
+ grid-template-columns: 1fr;
64
+ }
65
+ }
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class BaseComponent < ViewComponent::Base
7
+ include Katalyst::HtmlAttributes
8
+
9
+ MENU_CONTROLLER = "navigation--editor--menu"
10
+ LIST_CONTROLLER = "navigation--editor--list"
11
+ ITEM_CONTROLLER = "navigation--editor--item"
12
+ STATUS_BAR_CONTROLLER = "navigation--editor--status-bar"
13
+ NEW_ITEM_CONTROLLER = "navigation--editor--new-item"
14
+
15
+ attr_accessor :menu, :item
16
+
17
+ delegate :config, to: ::Katalyst::Navigation
18
+
19
+ def initialize(menu:, item: nil, **)
20
+ super(**)
21
+
22
+ @menu = menu
23
+ @item = item
24
+ end
25
+
26
+ def call; end
27
+
28
+ def menu_form_id
29
+ dom_id(menu, :items)
30
+ end
31
+
32
+ private
33
+
34
+ def attributes_scope
35
+ "menu[items_attributes][]"
36
+ end
37
+
38
+ def inspect
39
+ if item.present?
40
+ "<#{self.class.name} menu: #{menu.inspect}, item: #{item.inspect}>"
41
+ else
42
+ "<#{self.class.name} menu: #{menu.inspect}>"
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ <turbo-frame id="<%= dom_id(menu, :errors) %>">
2
+ <% if menu.errors.any? %>
3
+ <%= tag.div(class: "navigation-errors", **html_attributes) do %>
4
+ <h2>Errors in navigation</h2>
5
+ <ul class="errors">
6
+ <% menu.errors.each do |error| %>
7
+ <li class="error"><%= error.message %></li>
8
+ <% end %>
9
+ </ul>
10
+ <% end %>
11
+ <% end %>
12
+ </turbo-frame>
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class ErrorsComponent < BaseComponent
7
+ include Katalyst::Tables::TurboReplaceable
8
+
9
+ def id
10
+ dom_id(menu, :errors)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ <%= tag.div(**html_attributes) do %>
2
+ <div class="tree" data-invisible="<%= !item.visible? %>">
3
+ <div role="toolbar" data-tree-accordion-controls>
4
+ <span role="button" value="collapse" data-action="click-><%= MENU_CONTROLLER %>#collapse" title="Collapse tree"></span>
5
+ <span role="button" value="expand" data-action="click-><%= MENU_CONTROLLER %>#expand" title="Expand tree"></span>
6
+ </div>
7
+
8
+ <span role="img" value="<%= item.model_name.param_key %>" title="Type"></span>
9
+ <h4 class="heading" title="<%= item.title %>"><%= item.title %></h4>
10
+ <span role="img" value="invisible" title="Hidden"></span>
11
+ </div>
12
+
13
+ <div class="url">
14
+ <%= link_to item.url || "", item.url || "", data: { turbo: false } %>
15
+ </div>
16
+
17
+ <div role="toolbar" data-tree-controls>
18
+ <span role="button" value="de-nest" data-action="click-><%= MENU_CONTROLLER %>#deNest" title="Outdent"></span>
19
+ <span role="button" value="nest" data-action="click-><%= MENU_CONTROLLER %>#nest" title="Indent"></span>
20
+ <%= kpop_link_to("", edit_item_link, role: "button", title: "Edit", value: "edit") %>
21
+ <span role="button" value="remove" data-action="click-><%= MENU_CONTROLLER %>#remove" title="Remove"></span>
22
+ </div>
23
+
24
+ <input autocomplete="off" type="hidden" name="<%= attributes_scope %>[id]" value="<%= item.id %>">
25
+ <input autocomplete="off" type="hidden" name="<%= attributes_scope %>[depth]" value="<%= item.depth %>">
26
+ <input autocomplete="off" type="hidden" name="<%= attributes_scope %>[index]" value="<%= item.index %>">
27
+ <% end %>
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class ItemComponent < BaseComponent
7
+ include KpopHelper
8
+
9
+ def edit_item_link
10
+ if item.persisted?
11
+ helpers.katalyst_navigation.edit_menu_item_path(menu, item)
12
+ else
13
+ helpers.katalyst_navigation.new_menu_item_path(item.menu, type: item.type)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def default_html_attributes
20
+ {
21
+ id: dom_id(item),
22
+ data: {
23
+ controller: ITEM_CONTROLLER,
24
+ },
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class ItemEditorComponent < BaseComponent
7
+ include ::Turbo::FramesHelper
8
+
9
+ module Helpers
10
+ def prefix_partial_path_with_controller_namespace
11
+ false
12
+ end
13
+ end
14
+
15
+ def call
16
+ tag.div(**html_attributes) do
17
+ helpers.extend(Helpers)
18
+ helpers.render(item.model_name.param_key, item:, path:)
19
+ end
20
+ end
21
+
22
+ def id
23
+ "item-editor-#{item.id}"
24
+ end
25
+
26
+ def title
27
+ if item.persisted?
28
+ "Edit #{item.model_name.human.downcase}"
29
+ else
30
+ "New #{item.model_name.human.downcase}"
31
+ end
32
+ end
33
+
34
+ def path
35
+ if item.persisted?
36
+ view_context.katalyst_navigation.menu_item_path(menu, item)
37
+ else
38
+ view_context.katalyst_navigation.menu_items_path(menu)
39
+ end
40
+ end
41
+
42
+ def default_html_attributes
43
+ {
44
+ id:,
45
+ class: "navigation--item-editor",
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ <%= tag.div(**html_attributes) do %>
2
+ <label><%= label %></label>
3
+ <%#
4
+ # Template is stored inside the new item dom, and copied into drag
5
+ # events when the user initiates drag so that it can be copied into the
6
+ # editor list on drop.
7
+ #
8
+ %>
9
+ <template data-<%= NEW_ITEM_CONTROLLER %>-target="template">
10
+ <%= render row_component do %>
11
+ <%= render item_component %>
12
+ <% end %>
13
+ </template>
14
+ <% end %>
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class NewItemComponent < BaseComponent
7
+ ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
8
+ dragstart->#{NEW_ITEM_CONTROLLER}#dragstart
9
+ ACTIONS
10
+
11
+ with_collection_parameter :item
12
+
13
+ def initialize(item:, menu: item.menu)
14
+ super(item:, menu:)
15
+ end
16
+
17
+ def item_component(**)
18
+ ItemComponent.new(item:, menu:, **)
19
+ end
20
+
21
+ def row_component(**)
22
+ RowComponent.new(item:, menu:, **)
23
+ end
24
+
25
+ def label
26
+ t("katalyst.navigation.editor.new_item.#{item_type}", default: item.model_name.human)
27
+ end
28
+
29
+ def item_type
30
+ item.model_name.param_key
31
+ end
32
+
33
+ private
34
+
35
+ def default_html_attributes
36
+ {
37
+ draggable: "true",
38
+ role: "listitem",
39
+ data: {
40
+ item_type:,
41
+ controller: NEW_ITEM_CONTROLLER,
42
+ action: ACTIONS,
43
+ },
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ <div class="navigation--editor--new-items" role="listbox">
2
+ <%= render Katalyst::Navigation::Editor::NewItemComponent.with_collection(items) %>
3
+ </div>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class NewItemsComponent < BaseComponent
7
+ include ::Turbo::FramesHelper
8
+
9
+ renders_many :items, Editor::NewItemComponent
10
+
11
+ def items
12
+ Katalyst::Navigation.config.items.map do |item_class|
13
+ item_class = item_class.safe_constantize if item_class.is_a?(String)
14
+ item_class.new(menu:)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -11,5 +11,5 @@
11
11
  data-deny-remove
12
12
  data-deny-drag
13
13
  data-deny-edit>
14
- <%= yield %>
14
+ <%= content %>
15
15
  </li>
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Katalyst
4
4
  module Navigation
5
- VERSION = "1.4.1"
5
+ module Editor
6
+ class RowComponent < BaseComponent
7
+ end
8
+ end
6
9
  end
7
10
  end
@@ -3,13 +3,15 @@
3
3
  module Katalyst
4
4
  module Navigation
5
5
  module Editor
6
- class StatusBar < Base
6
+ class StatusBarComponent < BaseComponent
7
7
  ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
8
8
  navigation:change@document->#{STATUS_BAR_CONTROLLER}#change
9
9
  ACTIONS
10
10
 
11
- def build(**options)
12
- tag.div **default_options(**options) do
11
+ attr_reader :container
12
+
13
+ def call
14
+ tag.div(**html_attributes) do
13
15
  concat status(:published, last_update: l(menu.updated_at, format: :short))
14
16
  concat status(:draft)
15
17
  concat status(:dirty)
@@ -17,8 +19,8 @@ module Katalyst
17
19
  end
18
20
  end
19
21
 
20
- def status(state, **options)
21
- tag.span(t("views.katalyst.navigation.editor.#{state}_html", **options),
22
+ def status(state, **)
23
+ tag.span(t("views.katalyst.navigation.editor.#{state}_html", **),
22
24
  class: "status-text",
23
25
  data: { state => "" })
24
26
  end
@@ -32,24 +34,26 @@ module Katalyst
32
34
  end
33
35
  end
34
36
 
35
- def action(action, **options)
37
+ def action(action, **)
36
38
  tag.li do
37
39
  button_tag(t("views.katalyst.navigation.editor.#{action}"),
38
40
  name: "commit",
39
41
  value: action,
40
42
  form: menu_form_id,
41
- **options)
43
+ **)
42
44
  end
43
45
  end
44
46
 
45
47
  private
46
48
 
47
- def default_options(**options)
48
- add_option(options, :data, :controller, STATUS_BAR_CONTROLLER)
49
- add_option(options, :data, :action, ACTIONS)
50
- add_option(options, :data, :state, menu.state)
51
-
52
- options
49
+ def default_html_attributes
50
+ {
51
+ data: {
52
+ controller: STATUS_BAR_CONTROLLER,
53
+ action: ACTIONS,
54
+ state: menu.state,
55
+ },
56
+ }
53
57
  end
54
58
  end
55
59
  end
@@ -0,0 +1,11 @@
1
+ <div role="rowheader">
2
+ <h4>Title</h4>
3
+ <h4>URL</h4>
4
+ <h4>Actions</h4>
5
+ </div>
6
+
7
+ <%= tag.ol(id: menu_form_id, **html_attributes) do %>
8
+ <% items.each do |item| %>
9
+ <%= item %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ module Editor
6
+ class TableComponent < BaseComponent
7
+ ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
8
+ dragstart->#{LIST_CONTROLLER}#dragstart
9
+ dragover->#{LIST_CONTROLLER}#dragover
10
+ dragenter->#{LIST_CONTROLLER}#dragenter
11
+ dragleave->#{LIST_CONTROLLER}#dragleave
12
+ drop->#{LIST_CONTROLLER}#drop
13
+ dragend->#{LIST_CONTROLLER}#dragend
14
+ ACTIONS
15
+
16
+ renders_many :items, ->(item) do
17
+ row = RowComponent.new(item:, menu:)
18
+ row.with_content(render(ItemComponent.new(item:, menu:)))
19
+ row
20
+ end
21
+
22
+ private
23
+
24
+ def default_html_attributes
25
+ {
26
+ data: {
27
+ controller: LIST_CONTROLLER,
28
+ action: ACTIONS,
29
+ "#{MENU_CONTROLLER}_target": "menu",
30
+ },
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ <%= form_with(model: menu, url: helpers.katalyst_navigation.menu_path, **html_attributes) do |form| %>
2
+ <%# Hidden input ensures that if the container is empty then the controller receives an empty array. %>
3
+ <input type="hidden" name="<%= attributes_scope %>[id]">
4
+ <%= render errors %>
5
+
6
+ <%= render Katalyst::Navigation::Editor::TableComponent.new(menu:) do |list| %>
7
+ <%= menu.draft_items.each { |item| list.with_item(item) } %>
8
+ <% end %>
9
+ <% end %>