katalyst-navigation 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +25 -0
  5. data/app/assets/config/katalyst-navigation.js +1 -0
  6. data/app/assets/javascripts/controllers/navigation/editor/item_controller.js +45 -0
  7. data/app/assets/javascripts/controllers/navigation/editor/list_controller.js +105 -0
  8. data/app/assets/javascripts/controllers/navigation/editor/menu_controller.js +110 -0
  9. data/app/assets/javascripts/controllers/navigation/editor/new_item_controller.js +12 -0
  10. data/app/assets/javascripts/controllers/navigation/editor/status_bar_controller.js +22 -0
  11. data/app/assets/javascripts/utils/navigation/editor/item.js +245 -0
  12. data/app/assets/javascripts/utils/navigation/editor/menu.js +54 -0
  13. data/app/assets/javascripts/utils/navigation/editor/rules-engine.js +140 -0
  14. data/app/assets/stylesheets/katalyst/navigation/editor/_icon.scss +17 -0
  15. data/app/assets/stylesheets/katalyst/navigation/editor/_index.scss +145 -0
  16. data/app/assets/stylesheets/katalyst/navigation/editor/_item-actions.scss +92 -0
  17. data/app/assets/stylesheets/katalyst/navigation/editor/_item-rules.scss +24 -0
  18. data/app/assets/stylesheets/katalyst/navigation/editor/_new-items.scss +22 -0
  19. data/app/assets/stylesheets/katalyst/navigation/editor/_status-bar.scss +87 -0
  20. data/app/controllers/katalyst/navigation/base_controller.rb +12 -0
  21. data/app/controllers/katalyst/navigation/items_controller.rb +57 -0
  22. data/app/controllers/katalyst/navigation/menus_controller.rb +82 -0
  23. data/app/helpers/katalyst/navigation/editor/base.rb +41 -0
  24. data/app/helpers/katalyst/navigation/editor/item.rb +69 -0
  25. data/app/helpers/katalyst/navigation/editor/list.rb +41 -0
  26. data/app/helpers/katalyst/navigation/editor/menu.rb +37 -0
  27. data/app/helpers/katalyst/navigation/editor/new_item.rb +53 -0
  28. data/app/helpers/katalyst/navigation/editor/status_bar.rb +57 -0
  29. data/app/helpers/katalyst/navigation/editor_helper.rb +46 -0
  30. data/app/helpers/katalyst/navigation/frontend/builder.rb +53 -0
  31. data/app/helpers/katalyst/navigation/frontend_helper.rb +42 -0
  32. data/app/models/concerns/katalyst/navigation/garbage_collection.rb +31 -0
  33. data/app/models/concerns/katalyst/navigation/has_tree.rb +63 -0
  34. data/app/models/katalyst/navigation/button.rb +15 -0
  35. data/app/models/katalyst/navigation/item.rb +21 -0
  36. data/app/models/katalyst/navigation/link.rb +10 -0
  37. data/app/models/katalyst/navigation/menu.rb +123 -0
  38. data/app/models/katalyst/navigation/node.rb +21 -0
  39. data/app/models/katalyst/navigation/types/nodes_type.rb +42 -0
  40. data/app/views/katalyst/navigation/items/_button.html.erb +28 -0
  41. data/app/views/katalyst/navigation/items/_link.html.erb +21 -0
  42. data/app/views/katalyst/navigation/items/edit.html.erb +4 -0
  43. data/app/views/katalyst/navigation/items/new.html.erb +4 -0
  44. data/app/views/katalyst/navigation/items/update.turbo_stream.erb +7 -0
  45. data/app/views/katalyst/navigation/menus/_item.html.erb +15 -0
  46. data/app/views/katalyst/navigation/menus/_list_item.html.erb +14 -0
  47. data/app/views/katalyst/navigation/menus/_new_item.html.erb +3 -0
  48. data/app/views/katalyst/navigation/menus/_new_items.html.erb +5 -0
  49. data/app/views/katalyst/navigation/menus/edit.html.erb +15 -0
  50. data/app/views/katalyst/navigation/menus/index.html.erb +17 -0
  51. data/app/views/katalyst/navigation/menus/new.html.erb +15 -0
  52. data/app/views/katalyst/navigation/menus/show.html.erb +15 -0
  53. data/config/importmap.rb +5 -0
  54. data/config/locales/en.yml +12 -0
  55. data/config/routes.rb +9 -0
  56. data/db/migrate/20220826034057_create_katalyst_navigation_menus.rb +25 -0
  57. data/db/migrate/20220826034507_create_katalyst_navigation_items.rb +17 -0
  58. data/lib/katalyst/navigation/engine.rb +36 -0
  59. data/lib/katalyst/navigation/version.rb +7 -0
  60. data/lib/katalyst/navigation.rb +9 -0
  61. data/lib/tasks/yarn.rake +18 -0
  62. data/spec/factories/katalyst/navigation/items.rb +14 -0
  63. data/spec/factories/katalyst/navigation/menus.rb +17 -0
  64. metadata +109 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0149b9c1ba7f7a1ecfcc1e085fbe33b2b0eb11d0d5372b743f05d0261daf88e3'
4
+ data.tar.gz: 2648b6258ac7a7f2b4800f0b1371687cba043d9d4a647f5753076bc8a1b04327
5
+ SHA512:
6
+ metadata.gz: d31db0f384749242de03b5bc04da8a0d3137092c702a3c4eda5769bf53fe1f764bb457bd672af5a60c1f3ef1daa22a7d2830e4f53b78b0f2161bdc2db675b54a
7
+ data.tar.gz: 615fd376052d6b4e48351ef2d0d144a8bd66bfbe615d01b65eed57eb90a1679f4eae630abddbfe82d3aff9f1a47baa09987f4c3a536625f5ea00849612109805
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2022-09-05
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Katalyst Interactive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Navigation
2
+
3
+ Generates and edits navigation menus.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add katalyst-navigation
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install katalyst-navigation
14
+
15
+ ## Usage
16
+
17
+ This gem is still experimental and is not yet ready for public consumption. See dummy app for example usage.
18
+
19
+ ## Contributing
20
+
21
+ Bug reports and pull requests are welcome on GitHub at https://github.com/katalyst/navigation.
22
+
23
+ ## License
24
+
25
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1 @@
1
+ //= link_tree ../javascripts
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import Item from "utils/navigation/editor/item";
3
+
4
+ export default class ItemController extends Controller {
5
+ get item() {
6
+ return new Item(this.li);
7
+ }
8
+
9
+ get ol() {
10
+ return this.element.closest("ol");
11
+ }
12
+
13
+ get li() {
14
+ return this.element.closest("li");
15
+ }
16
+
17
+ connect() {
18
+ if (this.element.dataset.hasOwnProperty("delete")) {
19
+ this.remove();
20
+ }
21
+ // if index is not already set, re-index will set it
22
+ else if (!(this.item.index >= 0)) {
23
+ this.reindex();
24
+ }
25
+ // if item has been replaced via turbo, re-index will run the rules engine
26
+ // update our depth and index with values from the li's data attributes
27
+ else if (this.item.hasItemIdChanged()) {
28
+ this.item.updateAfterChange();
29
+ this.reindex();
30
+ }
31
+ }
32
+
33
+ remove() {
34
+ // capture ol
35
+ const ol = this.ol;
36
+ // remove self from dom
37
+ this.li.remove();
38
+ // reindex ol
39
+ this.reindex();
40
+ }
41
+
42
+ reindex() {
43
+ this.dispatch("reindex", { bubbles: true, prefix: "navigation" });
44
+ }
45
+ }
@@ -0,0 +1,105 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class ListController extends Controller {
4
+ dragstart(event) {
5
+ if (this.element !== event.target.parentElement) return;
6
+
7
+ const target = event.target;
8
+ event.dataTransfer.effectAllowed = "move";
9
+
10
+ // update element style after drag has begun
11
+ setTimeout(() => (target.dataset.dragging = ""));
12
+ }
13
+
14
+ dragover(event) {
15
+ const item = this.dragItem();
16
+ if (!item) return;
17
+
18
+ swap(this.dropTarget(event.target), item);
19
+
20
+ event.preventDefault();
21
+ return true;
22
+ }
23
+
24
+ dragenter(event) {
25
+ event.preventDefault();
26
+
27
+ if (event.dataTransfer.effectAllowed === "copy" && !this.dragItem()) {
28
+ const item = document.createElement("li");
29
+ item.dataset.dragging = "";
30
+ item.dataset.newItem = "";
31
+ this.element.prepend(item);
32
+ }
33
+ }
34
+
35
+ dragleave(event) {
36
+ const item = this.dragItem();
37
+ const related = this.dropTarget(event.relatedTarget);
38
+
39
+ // ignore if item is not set or we're moving into a valid drop target
40
+ if (!item || related) return;
41
+
42
+ // remove item if it's a new item
43
+ if (item.dataset.hasOwnProperty("newItem")) {
44
+ item.remove();
45
+ }
46
+ }
47
+
48
+ drop(event) {
49
+ const item = this.dragItem();
50
+
51
+ if (!item) return;
52
+
53
+ event.preventDefault();
54
+ delete item.dataset.dragging;
55
+ swap(this.dropTarget(event.target), item);
56
+
57
+ if (item.dataset.hasOwnProperty("newItem")) {
58
+ const template = document.createElement("template");
59
+ template.innerHTML = event.dataTransfer.getData("text/html");
60
+ const newItem = template.content.querySelector("li");
61
+
62
+ this.element.replaceChild(newItem, item);
63
+ setTimeout(() =>
64
+ newItem.querySelector("[role='button'][value='edit']").click()
65
+ );
66
+ }
67
+
68
+ this.reindex();
69
+ }
70
+
71
+ dragend() {
72
+ const item = this.dragItem();
73
+ if (!item) return;
74
+
75
+ delete item.dataset.dragging;
76
+ this.reset();
77
+ }
78
+
79
+ dragItem() {
80
+ return this.element.querySelector("[data-dragging]");
81
+ }
82
+
83
+ dropTarget(e) {
84
+ return e && e.closest("[data-controller='navigation--editor--list'] > *");
85
+ }
86
+
87
+ reindex() {
88
+ this.dispatch("reindex", { bubbles: true, prefix: "navigation" });
89
+ }
90
+
91
+ reset() {
92
+ this.dispatch("reset", { bubbles: true, prefix: "navigation" });
93
+ }
94
+ }
95
+
96
+ function swap(target, item) {
97
+ if (target && target !== item) {
98
+ const positionComparison = target.compareDocumentPosition(item);
99
+ if (positionComparison & Node.DOCUMENT_POSITION_FOLLOWING) {
100
+ target.insertAdjacentElement("beforebegin", item);
101
+ } else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {
102
+ target.insertAdjacentElement("afterend", item);
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,110 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ import Item from "utils/navigation/editor/item";
4
+ import Menu from "utils/navigation/editor/menu";
5
+ import RulesEngine from "utils/navigation/editor/rules-engine";
6
+
7
+ export default class MenuController extends Controller {
8
+ static targets = ["menu"];
9
+
10
+ connect() {
11
+ this.state = this.menu.state;
12
+
13
+ this.reindex();
14
+ }
15
+
16
+ get menu() {
17
+ return new Menu(this.menuTarget);
18
+ }
19
+
20
+ reindex() {
21
+ this.menu.reindex();
22
+ this.#update();
23
+ }
24
+
25
+ reset() {
26
+ this.menu.reset();
27
+ }
28
+
29
+ remove(event) {
30
+ const item = getEventItem(event);
31
+
32
+ item.node.remove();
33
+
34
+ this.#update();
35
+ event.preventDefault();
36
+ }
37
+
38
+ nest(event) {
39
+ const item = getEventItem(event);
40
+
41
+ item.traverse((child) => {
42
+ child.depth += 1;
43
+ });
44
+
45
+ this.#update();
46
+ event.preventDefault();
47
+ }
48
+
49
+ deNest(event) {
50
+ const item = getEventItem(event);
51
+
52
+ item.traverse((child) => {
53
+ child.depth -= 1;
54
+ });
55
+
56
+ this.#update();
57
+ event.preventDefault();
58
+ }
59
+
60
+ collapse(event) {
61
+ const item = getEventItem(event);
62
+
63
+ item.collapse();
64
+
65
+ this.#update();
66
+ event.preventDefault();
67
+ }
68
+
69
+ expand(event) {
70
+ const item = getEventItem(event);
71
+
72
+ item.expand();
73
+
74
+ this.#update();
75
+ event.preventDefault();
76
+ }
77
+
78
+ /**
79
+ * Re-apply rules to items to enable/disable appropriate actions.
80
+ */
81
+ #update() {
82
+ // debounce requests to ensure that we only update once per tick
83
+ this.updateRequested = true;
84
+ setTimeout(() => {
85
+ if (!this.updateRequested) return;
86
+
87
+ this.updateRequested = false;
88
+ const engine = new RulesEngine();
89
+ this.menu.items.forEach((item) => engine.update(item));
90
+
91
+ this.#notifyChange();
92
+ }, 0);
93
+ }
94
+
95
+ #notifyChange() {
96
+ this.dispatch("change", {
97
+ bubbles: true,
98
+ prefix: "navigation",
99
+ detail: { dirty: this.#isDirty() },
100
+ });
101
+ }
102
+
103
+ #isDirty() {
104
+ return this.menu.state !== this.state;
105
+ }
106
+ }
107
+
108
+ function getEventItem(event) {
109
+ return new Item(event.target.closest("[data-navigation-item]"));
110
+ }
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class NewItemController extends Controller {
4
+ static targets = ["template"];
5
+
6
+ dragstart(event) {
7
+ if (this.element !== event.target) return;
8
+
9
+ event.dataTransfer.setData("text/html", this.templateTarget.innerHTML);
10
+ event.dataTransfer.effectAllowed = "copy";
11
+ }
12
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class StatusBarController extends Controller {
4
+ connect() {
5
+ // cache the version's state in the controller on connect
6
+ this.versionState = this.element.dataset.state;
7
+ }
8
+
9
+ change(e) {
10
+ if (e.detail && e.detail.hasOwnProperty("dirty")) {
11
+ this.update(e.detail);
12
+ }
13
+ }
14
+
15
+ update({ dirty }) {
16
+ if (dirty) {
17
+ this.element.dataset.state = "dirty";
18
+ } else {
19
+ this.element.dataset.state = this.versionState;
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,245 @@
1
+ export default class Item {
2
+ /**
3
+ * Sort items by their index.
4
+ *
5
+ * @param a {Item}
6
+ * @param b {Item}
7
+ * @returns {number}
8
+ */
9
+ static comparator(a, b) {
10
+ return a.index - b.index;
11
+ }
12
+
13
+ /**
14
+ * @param node {Element} li[data-navigation-index]
15
+ */
16
+ constructor(node) {
17
+ this.node = node;
18
+ }
19
+
20
+ /**
21
+ * @returns {String} id of the node's item (from data attributes)
22
+ */
23
+ get itemId() {
24
+ return this.node.dataset[`navigationItemId`];
25
+ }
26
+
27
+ get #itemIdInput() {
28
+ return this.node.querySelector(`input[name$="[id]"]`);
29
+ }
30
+
31
+ /**
32
+ * @param itemId {String} id
33
+ */
34
+ set itemId(id) {
35
+ if (this.itemId === id) return;
36
+
37
+ this.node.dataset[`navigationItemId`] = `${id}`;
38
+ this.#itemIdInput.value = `${id}`;
39
+ }
40
+
41
+ /**
42
+ * @returns {number} logical nesting depth of node in menu
43
+ */
44
+ get depth() {
45
+ return parseInt(this.node.dataset[`navigationDepth`]);
46
+ }
47
+
48
+ get #depthInput() {
49
+ return this.node.querySelector(`input[name$="[depth]"]`);
50
+ }
51
+
52
+ /**
53
+ * @param depth {number} depth >= 0
54
+ */
55
+ set depth(depth) {
56
+ if (this.depth === depth) return;
57
+
58
+ this.node.dataset[`navigationDepth`] = `${depth}`;
59
+ this.#depthInput.value = `${depth}`;
60
+ }
61
+
62
+ /**
63
+ * @returns {number} logical index of node in menu (pre-order traversal)
64
+ */
65
+ get index() {
66
+ return parseInt(this.node.dataset[`navigationIndex`]);
67
+ }
68
+
69
+ get #indexInput() {
70
+ return this.node.querySelector(`input[name$="[index]"]`);
71
+ }
72
+
73
+ /**
74
+ * @param index {number} index >= 0
75
+ */
76
+ set index(index) {
77
+ if (this.index === index) return;
78
+
79
+ this.node.dataset[`navigationIndex`] = `${index}`;
80
+ this.#indexInput.value = `${index}`;
81
+ }
82
+
83
+ /**
84
+ * @returns {Item} nearest neighbour (index - 1)
85
+ */
86
+ get previousItem() {
87
+ let sibling = this.node.previousElementSibling;
88
+ if (sibling) return new Item(sibling);
89
+ }
90
+
91
+ /**
92
+ * @returns {Item} nearest neighbour (index + 1)
93
+ */
94
+ get nextItem() {
95
+ let sibling = this.node.nextElementSibling;
96
+ if (sibling) return new Item(sibling);
97
+ }
98
+
99
+ /**
100
+ * @returns {boolean} true if this item has any collapsed children
101
+ */
102
+ hasCollapsedDescendants() {
103
+ let childrenList = this.#childrenListElement;
104
+ return !!childrenList && childrenList.children.length > 0;
105
+ }
106
+
107
+ /**
108
+ * @returns {boolean} true if this item has any expanded children
109
+ */
110
+ hasExpandedDescendants() {
111
+ let sibling = this.nextItem;
112
+ return !!sibling && sibling.depth > this.depth;
113
+ }
114
+
115
+ /**
116
+ * Recursively traverse the node and its descendants.
117
+ *
118
+ * @callback {Item}
119
+ */
120
+ traverse(callback) {
121
+ // capture descendants before traversal in case of side-effects
122
+ // specifically, setting depth affects calculation
123
+ const collapsed = this.#collapsedChildren;
124
+ const expanded = this.#expandedDescendants;
125
+
126
+ callback(this);
127
+ collapsed.forEach((item) => item.traverse(callback));
128
+ expanded.forEach((item) => item.traverse(callback));
129
+ }
130
+
131
+ /**
132
+ * Collapses visible (logical) children into this element's hidden children
133
+ * list, creating it if it doesn't already exist.
134
+ */
135
+ collapse() {
136
+ let listElement = this.#childrenListElement;
137
+
138
+ if (!listElement) listElement = createChildrenList(this.node);
139
+
140
+ this.#expandedDescendants.forEach((child) =>
141
+ listElement.appendChild(child.node)
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Moves any collapsed children back into the parent menu.
147
+ */
148
+ expand() {
149
+ if (!this.hasCollapsedDescendants()) return;
150
+
151
+ Array.from(this.#childrenListElement.children)
152
+ .reverse()
153
+ .forEach((node) => {
154
+ this.node.insertAdjacentElement("afterend", node);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Sets the state of a given rule on the target node.
160
+ *
161
+ * @param rule {String}
162
+ * @param deny {boolean}
163
+ */
164
+ toggleRule(rule, deny = false) {
165
+ if (this.node.dataset.hasOwnProperty(rule) && !deny)
166
+ delete this.node.dataset[rule];
167
+ if (!this.node.dataset.hasOwnProperty(rule) && deny)
168
+ this.node.dataset[rule] = "";
169
+
170
+ if (rule === "denyDrag") {
171
+ if (!this.node.hasAttribute("draggable") && !deny) {
172
+ this.node.setAttribute("draggable", "true");
173
+ }
174
+ if (this.node.hasAttribute("draggable") && deny) {
175
+ this.node.removeAttribute("draggable");
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Detects turbo item changes by comparing the dataset id with the input
182
+ */
183
+ hasItemIdChanged() {
184
+ return !(this.#itemIdInput.value === this.itemId);
185
+ }
186
+
187
+ /**
188
+ * Updates inputs, in case they don't match the data values, e.g., when the
189
+ * nested inputs have been hot-swapped by turbo with data from the server.
190
+ *
191
+ * Updates itemId from input as that is the canonical source.
192
+ */
193
+ updateAfterChange() {
194
+ this.itemId = this.#itemIdInput.value;
195
+ this.#indexInput.value = this.index;
196
+ this.#depthInput.value = this.depth;
197
+ }
198
+
199
+ /**
200
+ * Finds the dom container for storing collapsed (hidden) children, if present.
201
+ *
202
+ * @returns {Element} ol[data-navigation-children]
203
+ */
204
+ get #childrenListElement() {
205
+ return this.node.querySelector(`:scope > [data-navigation-children]`);
206
+ }
207
+
208
+ get #expandedDescendants() {
209
+ const descendants = [];
210
+
211
+ let sibling = this.nextItem;
212
+ while (sibling && sibling.depth > this.depth) {
213
+ descendants.push(sibling);
214
+ sibling = sibling.nextItem;
215
+ }
216
+
217
+ return descendants;
218
+ }
219
+
220
+ get #collapsedChildren() {
221
+ if (!this.hasCollapsedDescendants()) return [];
222
+
223
+ return Array.from(this.#childrenListElement.children).map(
224
+ (node) => new Item(node)
225
+ );
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Finds or creates a dom container for storing collapsed (hidden) children.
231
+ *
232
+ * @param node {Element} li[data-navigation-index]
233
+ * @returns {Element} ol[data-navigation-children]
234
+ */
235
+ function createChildrenList(node) {
236
+ const childrenList = document.createElement("ol");
237
+ childrenList.setAttribute("class", "hidden");
238
+
239
+ // if objectType is "rich-content" set richContentChildren as a data attribute
240
+ childrenList.dataset[`navigationChildren`] = "";
241
+
242
+ node.appendChild(childrenList);
243
+
244
+ return childrenList;
245
+ }
@@ -0,0 +1,54 @@
1
+ import Item from "./item";
2
+
3
+ /**
4
+ * @param nodes {NodeList}
5
+ * @returns {Item[]}
6
+ */
7
+ function createItemList(nodes) {
8
+ return Array.from(nodes).map((node) => new Item(node));
9
+ }
10
+
11
+ export default class Menu {
12
+ /**
13
+ * @param node {Element} navigation editor list
14
+ */
15
+ constructor(node) {
16
+ this.node = node;
17
+ }
18
+
19
+ /**
20
+ * @return {Item[]} an ordered list of all items in the menu
21
+ */
22
+ get items() {
23
+ return createItemList(
24
+ this.node.querySelectorAll("[data-navigation-index]")
25
+ );
26
+ }
27
+
28
+ /**
29
+ * @return {String} a serialized description of the structure of the menu
30
+ */
31
+ get state() {
32
+ const inputs = this.node.querySelectorAll("li input[type=hidden]");
33
+ return Array.from(inputs)
34
+ .map((e) => e.value)
35
+ .join("/");
36
+ }
37
+
38
+ /**
39
+ * Set the index of items based on their current position.
40
+ */
41
+ reindex() {
42
+ this.items.map((item, index) => (item.index = index));
43
+ }
44
+
45
+ /**
46
+ * Resets the order of items to their defined index.
47
+ * Useful after an aborted drag.
48
+ */
49
+ reset() {
50
+ this.items.sort(Item.comparator).forEach((item) => {
51
+ this.node.appendChild(item.node);
52
+ });
53
+ }
54
+ }