katalyst-navigation 1.4.0 → 1.5.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -5
  3. data/app/assets/builds/katalyst/navigation.esm.js +911 -0
  4. data/app/assets/builds/katalyst/navigation.js +911 -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/components/katalyst/navigation/editor/base_component.rb +48 -0
  9. data/app/components/katalyst/navigation/editor/errors_component.html.erb +12 -0
  10. data/app/components/katalyst/navigation/editor/errors_component.rb +15 -0
  11. data/app/components/katalyst/navigation/editor/item_component.html.erb +27 -0
  12. data/app/components/katalyst/navigation/editor/item_component.rb +30 -0
  13. data/app/components/katalyst/navigation/editor/item_editor_component.rb +51 -0
  14. data/app/components/katalyst/navigation/editor/new_item_component.html.erb +14 -0
  15. data/app/components/katalyst/navigation/editor/new_item_component.rb +49 -0
  16. data/app/components/katalyst/navigation/editor/new_items_component.html.erb +3 -0
  17. data/app/components/katalyst/navigation/editor/new_items_component.rb +20 -0
  18. data/app/{views/katalyst/navigation/menus/_list_item.html.erb → components/katalyst/navigation/editor/row_component.html.erb} +1 -1
  19. data/{lib/katalyst/navigation/version.rb → app/components/katalyst/navigation/editor/row_component.rb} +4 -1
  20. data/app/{helpers/katalyst/navigation/editor/status_bar.rb → components/katalyst/navigation/editor/status_bar_component.rb} +17 -13
  21. data/app/components/katalyst/navigation/editor/table_component.html.erb +11 -0
  22. data/app/components/katalyst/navigation/editor/table_component.rb +36 -0
  23. data/app/components/katalyst/navigation/editor_component.html.erb +9 -0
  24. data/app/components/katalyst/navigation/editor_component.rb +47 -0
  25. data/app/controllers/concerns/katalyst/navigation/has_navigation.rb +5 -14
  26. data/app/controllers/katalyst/navigation/items_controller.rb +24 -12
  27. data/app/controllers/katalyst/navigation/menus_controller.rb +25 -13
  28. data/app/helpers/katalyst/navigation/frontend_helper.rb +7 -7
  29. data/app/javascript/navigation/application.js +30 -0
  30. data/app/{assets/javascripts/controllers → javascript}/navigation/editor/item_controller.js +1 -1
  31. data/app/{assets/javascripts/utils → javascript}/navigation/editor/menu.js +1 -1
  32. data/app/{assets/javascripts/controllers → javascript}/navigation/editor/menu_controller.js +3 -3
  33. data/app/models/katalyst/navigation/menu.rb +2 -0
  34. data/app/models/katalyst/navigation/types/nodes_type.rb +2 -2
  35. data/app/views/katalyst/navigation/items/_button.html.erb +6 -0
  36. data/app/views/katalyst/navigation/items/_form_errors.html.erb +5 -0
  37. data/app/views/katalyst/navigation/items/_heading.html.erb +1 -0
  38. data/app/views/katalyst/navigation/items/_link.html.erb +11 -0
  39. data/app/views/katalyst/navigation/items/edit.html.erb +4 -3
  40. data/app/views/katalyst/navigation/items/edit.turbo_stream.erb +3 -0
  41. data/app/views/katalyst/navigation/items/new.html.erb +1 -1
  42. data/app/views/katalyst/navigation/items/update.turbo_stream.erb +2 -5
  43. data/app/views/katalyst/navigation/menus/edit.html.erb +1 -1
  44. data/app/views/katalyst/navigation/menus/index.html.erb +1 -1
  45. data/app/views/katalyst/navigation/menus/show.html.erb +3 -13
  46. data/config/importmap.rb +1 -4
  47. data/db/migrate/20230727025052_update_target_syntax.rb +1 -1
  48. data/lib/katalyst/navigation/config.rb +4 -0
  49. data/lib/katalyst/navigation/engine.rb +1 -1
  50. data/lib/katalyst/navigation.rb +6 -1
  51. data/spec/factories/katalyst/navigation/menus.rb +1 -1
  52. metadata +93 -27
  53. data/app/controllers/katalyst/navigation/base_controller.rb +0 -12
  54. data/app/helpers/katalyst/navigation/editor/base.rb +0 -41
  55. data/app/helpers/katalyst/navigation/editor/errors.rb +0 -24
  56. data/app/helpers/katalyst/navigation/editor/item.rb +0 -62
  57. data/app/helpers/katalyst/navigation/editor/list.rb +0 -41
  58. data/app/helpers/katalyst/navigation/editor/menu.rb +0 -47
  59. data/app/helpers/katalyst/navigation/editor/new_item.rb +0 -53
  60. data/app/helpers/katalyst/navigation/editor_helper.rb +0 -52
  61. data/app/views/katalyst/navigation/menus/_item.html.erb +0 -15
  62. data/app/views/katalyst/navigation/menus/_new_item.html.erb +0 -3
  63. data/app/views/katalyst/navigation/menus/_new_items.html.erb +0 -5
  64. /data/app/{assets/javascripts/utils → javascript}/navigation/editor/item.js +0 -0
  65. /data/app/{assets/javascripts/controllers → javascript}/navigation/editor/list_controller.js +0 -0
  66. /data/app/{assets/javascripts/controllers → javascript}/navigation/editor/new_item_controller.js +0 -0
  67. /data/app/{assets/javascripts/utils → javascript}/navigation/editor/rules-engine.js +0 -0
  68. /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){const n=e.compareDocumentPosition(t);n&Node.DOCUMENT_POSITION_FOLLOWING?e.insertAdjacentElement("beforebegin",t):n&Node.DOCUMENT_POSITION_PRECEDING&&e.insertAdjacentElement("afterend",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&&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 // if objectType is \"rich-content\" set richContentChildren as a data attribute\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 e && e.closest(\"[data-controller='navigation--editor--list'] > *\");\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 && target !== item) {\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","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","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":"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"}
@@ -1 +1 @@
1
- //= link_tree ../javascripts
1
+ //= link_tree ../builds
@@ -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="content--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.0"
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 %>
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ class EditorComponent < Editor::BaseComponent
6
+ ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
7
+ submit->#{MENU_CONTROLLER}#reindex
8
+ navigation:drop->#{MENU_CONTROLLER}#drop
9
+ navigation:reindex->#{MENU_CONTROLLER}#reindex
10
+ navigation:reset->#{MENU_CONTROLLER}#reset
11
+ ACTIONS
12
+
13
+ def status_bar
14
+ @status_bar ||= Editor::StatusBarComponent.new(menu:)
15
+ end
16
+
17
+ def new_items
18
+ @new_items ||= Editor::NewItemsComponent.new(menu:)
19
+ end
20
+
21
+ def item_editor(item:)
22
+ Editor::ItemEditorComponent.new(menu:, item:)
23
+ end
24
+
25
+ def item(item:)
26
+ Editor::ItemComponent.new(menu:, item:)
27
+ end
28
+
29
+ def errors
30
+ @errors ||= Katalyst::Navigation.config.errors_component.constantize.new(menu:)
31
+ end
32
+
33
+ private
34
+
35
+ def default_html_attributes
36
+ {
37
+ id: menu_form_id,
38
+ data: {
39
+ controller: MENU_CONTROLLER,
40
+ action: ACTIONS,
41
+ "#{MENU_CONTROLLER}-max-depth-value": menu.depth,
42
+ },
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -58,7 +58,11 @@ module Katalyst
58
58
  #
59
59
  # @return {Katalyst::Navigation::Menu} menu with the given slug
60
60
  def navigation_menu_for(slug)
61
- @navigation_menus[slug.to_s]
61
+ navigation_menus[slug.to_s]
62
+ end
63
+
64
+ def navigation_menus
65
+ @navigation_menus ||= Katalyst::Navigation::Menu.published.index_by(&:slug)
62
66
  end
63
67
 
64
68
  # @see ActionView::Helpers::ControllerHelper#assign_controller
@@ -76,19 +80,6 @@ module Katalyst
76
80
 
77
81
  helper Katalyst::Navigation::FrontendHelper
78
82
  helper NavigationHelper
79
-
80
- # @see ActionController::Rendering#render
81
- def render(*args)
82
- set_navigation_menus
83
-
84
- super
85
- end
86
- end
87
-
88
- protected
89
-
90
- def set_navigation_menus
91
- @navigation_menus = Katalyst::Navigation::Menu.includes(:published_version).index_by(&:slug)
92
83
  end
93
84
  end
94
85
  end