katalyst-navigation 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/javascripts/controllers/navigation/editor/list_controller.js +10 -5
- data/app/assets/javascripts/controllers/navigation/editor/menu_controller.js +27 -0
- data/app/assets/javascripts/utils/navigation/editor/item.js +34 -6
- data/app/assets/javascripts/utils/navigation/editor/rules-engine.js +70 -9
- data/app/assets/stylesheets/katalyst/navigation/editor/_index.scss +1 -1
- data/app/helpers/katalyst/navigation/editor/item.rb +0 -7
- data/app/helpers/katalyst/navigation/editor/menu.rb +1 -0
- data/app/helpers/katalyst/navigation/editor_helper.rb +1 -0
- data/app/helpers/katalyst/navigation/frontend/builder.rb +4 -0
- data/app/models/katalyst/navigation/heading.rb +9 -0
- data/app/models/katalyst/navigation/item.rb +5 -1
- data/app/models/katalyst/navigation/layout.rb +9 -0
- data/app/views/katalyst/navigation/items/_heading.html.erb +16 -0
- data/app/views/katalyst/navigation/menus/_item.html.erb +1 -1
- data/app/views/katalyst/navigation/menus/_list_item.html.erb +1 -0
- data/lib/katalyst/navigation/version.rb +1 -1
- data/spec/factories/katalyst/navigation/items.rb +4 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6424bb96df0ceb96b40aa6d225dbe12461784edb41c1b83863435a56db02fd68
|
4
|
+
data.tar.gz: 6a46bbd339f8c171fe7a9791ad94281c6e0cb6f846f8bab04bd62edb8e8c339c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 173290becb5111777b887baeefe3a93642d8b154cb6463fd0a84a83e0246be7159568cc836d3eb2ca4ca7b9d7a2b616eff69c284d7abbaa93f7f2a26e0f81f76
|
7
|
+
data.tar.gz: cad3f1cf87f00b7868aac073b71d192345b341feb73a0d28de332fccd932278988c256cc9228d2f30eadc55b2de0c4070d8a263c323e235b22f18f087bbc9abb
|
@@ -46,7 +46,7 @@ export default class ListController extends Controller {
|
|
46
46
|
}
|
47
47
|
|
48
48
|
drop(event) {
|
49
|
-
|
49
|
+
let item = this.dragItem();
|
50
50
|
|
51
51
|
if (!item) return;
|
52
52
|
|
@@ -55,17 +55,22 @@ export default class ListController extends Controller {
|
|
55
55
|
swap(this.dropTarget(event.target), item);
|
56
56
|
|
57
57
|
if (item.dataset.hasOwnProperty("newItem")) {
|
58
|
+
const placeholder = item;
|
58
59
|
const template = document.createElement("template");
|
59
60
|
template.innerHTML = event.dataTransfer.getData("text/html");
|
60
|
-
|
61
|
+
item = template.content.querySelector("li");
|
61
62
|
|
62
|
-
this.element.replaceChild(
|
63
|
+
this.element.replaceChild(item, placeholder);
|
63
64
|
setTimeout(() =>
|
64
|
-
|
65
|
+
item.querySelector("[role='button'][value='edit']").click()
|
65
66
|
);
|
66
67
|
}
|
67
68
|
|
68
|
-
this.
|
69
|
+
this.dispatch("drop", {
|
70
|
+
target: item,
|
71
|
+
bubbles: true,
|
72
|
+
prefix: "navigation",
|
73
|
+
});
|
69
74
|
}
|
70
75
|
|
71
76
|
dragend() {
|
@@ -29,6 +29,32 @@ export default class MenuController extends Controller {
|
|
29
29
|
this.menu.reset();
|
30
30
|
}
|
31
31
|
|
32
|
+
drop(event) {
|
33
|
+
this.menu.reindex(); // set indexes before calculating previous
|
34
|
+
|
35
|
+
const item = getEventItem(event);
|
36
|
+
const previous = item.previousItem;
|
37
|
+
|
38
|
+
let delta = 0;
|
39
|
+
if (previous === undefined) {
|
40
|
+
// if previous does not exist, set depth to 0
|
41
|
+
delta = -item.depth;
|
42
|
+
} else if (item.nextItem && item.nextItem.depth > previous.depth) {
|
43
|
+
// if next is a child of previous, make item a child of previous
|
44
|
+
delta = previous.depth - item.depth + 1;
|
45
|
+
} else {
|
46
|
+
// otherwise, make item a sibling of previous
|
47
|
+
delta = previous.depth - item.depth;
|
48
|
+
}
|
49
|
+
|
50
|
+
item.traverse((child) => {
|
51
|
+
child.depth += delta;
|
52
|
+
});
|
53
|
+
|
54
|
+
this.#update();
|
55
|
+
event.preventDefault();
|
56
|
+
}
|
57
|
+
|
32
58
|
remove(event) {
|
33
59
|
const item = getEventItem(event);
|
34
60
|
|
@@ -89,6 +115,7 @@ export default class MenuController extends Controller {
|
|
89
115
|
|
90
116
|
this.updateRequested = false;
|
91
117
|
const engine = new RulesEngine(this.maxDepthValue);
|
118
|
+
this.menu.items.forEach((item) => engine.normalize(item));
|
92
119
|
this.menu.items.forEach((item) => engine.update(item));
|
93
120
|
|
94
121
|
this.#notifyChange();
|
@@ -80,6 +80,13 @@ export default class Item {
|
|
80
80
|
this.#indexInput.value = `${index}`;
|
81
81
|
}
|
82
82
|
|
83
|
+
/**
|
84
|
+
* @returns {boolean} true if this item can have children
|
85
|
+
*/
|
86
|
+
get isLayout() {
|
87
|
+
return this.node.hasAttribute("data-content-layout");
|
88
|
+
}
|
89
|
+
|
83
90
|
/**
|
84
91
|
* @returns {Item} nearest neighbour (index - 1)
|
85
92
|
*/
|
@@ -120,12 +127,25 @@ export default class Item {
|
|
120
127
|
traverse(callback) {
|
121
128
|
// capture descendants before traversal in case of side-effects
|
122
129
|
// specifically, setting depth affects calculation
|
123
|
-
const collapsed = this.#collapsedChildren;
|
124
130
|
const expanded = this.#expandedDescendants;
|
125
131
|
|
126
132
|
callback(this);
|
127
|
-
|
128
|
-
expanded.forEach((item) => item
|
133
|
+
this.#traverseCollapsed(callback);
|
134
|
+
expanded.forEach((item) => item.#traverseCollapsed(callback));
|
135
|
+
}
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Recursively traverse the node's collapsed descendants, if any.
|
139
|
+
*
|
140
|
+
* @callback {Item}
|
141
|
+
*/
|
142
|
+
#traverseCollapsed(callback) {
|
143
|
+
if (!this.hasCollapsedDescendants()) return;
|
144
|
+
|
145
|
+
this.#collapsedDescendants.forEach((item) => {
|
146
|
+
callback(item);
|
147
|
+
item.#traverseCollapsed(callback);
|
148
|
+
});
|
129
149
|
}
|
130
150
|
|
131
151
|
/**
|
@@ -162,10 +182,12 @@ export default class Item {
|
|
162
182
|
* @param deny {boolean}
|
163
183
|
*/
|
164
184
|
toggleRule(rule, deny = false) {
|
165
|
-
if (this.node.dataset.hasOwnProperty(rule) && !deny)
|
185
|
+
if (this.node.dataset.hasOwnProperty(rule) && !deny) {
|
166
186
|
delete this.node.dataset[rule];
|
167
|
-
|
187
|
+
}
|
188
|
+
if (!this.node.dataset.hasOwnProperty(rule) && deny) {
|
168
189
|
this.node.dataset[rule] = "";
|
190
|
+
}
|
169
191
|
|
170
192
|
if (rule === "denyDrag") {
|
171
193
|
if (!this.node.hasAttribute("draggable") && !deny) {
|
@@ -205,6 +227,9 @@ export default class Item {
|
|
205
227
|
return this.node.querySelector(`:scope > [data-navigation-children]`);
|
206
228
|
}
|
207
229
|
|
230
|
+
/**
|
231
|
+
* @returns {Item[]} all items that follow this element that have a greater depth.
|
232
|
+
*/
|
208
233
|
get #expandedDescendants() {
|
209
234
|
const descendants = [];
|
210
235
|
|
@@ -217,7 +242,10 @@ export default class Item {
|
|
217
242
|
return descendants;
|
218
243
|
}
|
219
244
|
|
220
|
-
|
245
|
+
/**
|
246
|
+
* @returns {Item[]} all items directly contained inside this element's hidden children element.
|
247
|
+
*/
|
248
|
+
get #collapsedDescendants() {
|
221
249
|
if (!this.hasCollapsedDescendants()) return [];
|
222
250
|
|
223
251
|
return Array.from(this.#childrenListElement.children).map(
|
@@ -9,24 +9,38 @@ export default class RulesEngine {
|
|
9
9
|
"denyEdit",
|
10
10
|
];
|
11
11
|
|
12
|
-
constructor(maxDepth = null) {
|
12
|
+
constructor(maxDepth = null, debug = false) {
|
13
13
|
this.maxDepth = maxDepth;
|
14
|
+
if (debug) {
|
15
|
+
this.debug = (...args) => console.log(...args);
|
16
|
+
} else {
|
17
|
+
this.debug = () => {};
|
18
|
+
}
|
14
19
|
}
|
15
20
|
|
16
21
|
/**
|
17
|
-
*
|
18
|
-
*
|
22
|
+
* Enforce structural rules to ensure that the given item is currently in a
|
23
|
+
* valid state.
|
19
24
|
*
|
20
25
|
* @param {Item} item
|
21
26
|
*/
|
22
|
-
|
23
|
-
this.rules = {};
|
24
|
-
|
27
|
+
normalize(item) {
|
25
28
|
// structural rules enforce a valid tree structure
|
26
29
|
this.firstItemDepthZero(item);
|
27
30
|
this.depthMustBeSet(item);
|
28
31
|
this.itemCannotHaveInvalidDepth(item);
|
29
32
|
this.itemCannotExceedDepthLimit(item);
|
33
|
+
this.parentMustBeLayout(item);
|
34
|
+
this.parentCannotHaveExpandedAndCollapsedChildren(item);
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* Apply rules to the given item to determine what operations are permitted.
|
39
|
+
*
|
40
|
+
* @param {Item} item
|
41
|
+
*/
|
42
|
+
update(item) {
|
43
|
+
this.rules = {};
|
30
44
|
|
31
45
|
// behavioural rules define what the user is allowed to do
|
32
46
|
this.parentsCannotDeNest(item);
|
@@ -47,7 +61,9 @@ export default class RulesEngine {
|
|
47
61
|
* First item can't have a parent, so its depth should always be 0
|
48
62
|
*/
|
49
63
|
firstItemDepthZero(item) {
|
50
|
-
if (item.index === 0) {
|
64
|
+
if (item.index === 0 && item.depth !== 0) {
|
65
|
+
this.debug(`enforce depth on item ${item.index}: ${item.depth} => 0`);
|
66
|
+
|
51
67
|
item.depth = 0;
|
52
68
|
}
|
53
69
|
}
|
@@ -59,6 +75,8 @@ export default class RulesEngine {
|
|
59
75
|
*/
|
60
76
|
depthMustBeSet(item) {
|
61
77
|
if (isNaN(item.depth) || item.depth < 0) {
|
78
|
+
this.debug(`unset depth on item ${item.index}: => 0`);
|
79
|
+
|
62
80
|
item.depth = 0;
|
63
81
|
}
|
64
82
|
}
|
@@ -71,6 +89,12 @@ export default class RulesEngine {
|
|
71
89
|
itemCannotHaveInvalidDepth(item) {
|
72
90
|
const previous = item.previousItem;
|
73
91
|
if (previous && previous.depth < item.depth - 1) {
|
92
|
+
this.debug(
|
93
|
+
`invalid depth on item ${item.index}: ${item.depth} => ${
|
94
|
+
previous.depth + 1
|
95
|
+
}`
|
96
|
+
);
|
97
|
+
|
74
98
|
item.depth = previous.depth + 1;
|
75
99
|
}
|
76
100
|
}
|
@@ -91,6 +115,37 @@ export default class RulesEngine {
|
|
91
115
|
}
|
92
116
|
}
|
93
117
|
|
118
|
+
/**
|
119
|
+
* Parent item, if any, must be a layout.
|
120
|
+
*
|
121
|
+
* @param {Item} item
|
122
|
+
*/
|
123
|
+
parentMustBeLayout(item) {
|
124
|
+
// if we're the first child, make sure our parent is a layout
|
125
|
+
// if we're a sibling, we know the previous item is valid so we must be too
|
126
|
+
const previous = item.previousItem;
|
127
|
+
if (previous && previous.depth < item.depth && !previous.isLayout) {
|
128
|
+
this.debug(
|
129
|
+
`invalid parent for item ${item.index}: ${item.depth} => ${previous.depth}`
|
130
|
+
);
|
131
|
+
|
132
|
+
item.depth = previous.depth;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* If a parent has expanded and collapsed children, expand.
|
138
|
+
*
|
139
|
+
* @param {Item} item
|
140
|
+
*/
|
141
|
+
parentCannotHaveExpandedAndCollapsedChildren(item) {
|
142
|
+
if (item.hasCollapsedDescendants() && item.hasExpandedDescendants()) {
|
143
|
+
this.debug(`expanding collapsed children of item ${item.index}`);
|
144
|
+
|
145
|
+
item.expand();
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
94
149
|
/**
|
95
150
|
* De-nesting an item would create a gap of 2 between itself and its children
|
96
151
|
*
|
@@ -134,7 +189,13 @@ export default class RulesEngine {
|
|
134
189
|
*/
|
135
190
|
nestingNeedsParent(item) {
|
136
191
|
const previous = item.previousItem;
|
137
|
-
|
192
|
+
// no previous, so cannot nest
|
193
|
+
if (!previous) this.#deny("denyNest");
|
194
|
+
// previous is too shallow, nesting would increase depth too much
|
195
|
+
else if (previous.depth < item.depth) this.#deny("denyNest");
|
196
|
+
// new parent is not a layout
|
197
|
+
else if (previous.depth === item.depth && !previous.isLayout)
|
198
|
+
this.#deny("denyNest");
|
138
199
|
}
|
139
200
|
|
140
201
|
/**
|
@@ -154,7 +215,7 @@ export default class RulesEngine {
|
|
154
215
|
* @param {Item} item
|
155
216
|
*/
|
156
217
|
parentsCannotBeDeleted(item) {
|
157
|
-
if (item.hasExpandedDescendants()) this.#deny("denyRemove");
|
218
|
+
if (!item.itemId || item.hasExpandedDescendants()) this.#deny("denyRemove");
|
158
219
|
}
|
159
220
|
|
160
221
|
/**
|
@@ -118,7 +118,7 @@ $status-dirty-color: #aaa !default;
|
|
118
118
|
}
|
119
119
|
|
120
120
|
// Ensures vertical alignment of header with rows
|
121
|
-
[data-controller="navigation--editor
|
121
|
+
[data-controller="navigation--editor--menu"] {
|
122
122
|
[role="rowheader"] {
|
123
123
|
min-height: var(--row-height);
|
124
124
|
background: var(--table-header-color);
|
@@ -14,13 +14,6 @@ module Katalyst
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
def build_all(*items)
|
18
|
-
render partial: "katalyst/navigation/menus/link_item",
|
19
|
-
layout: "katalyst/navigation/menus/navigation_menu_link",
|
20
|
-
collection: items,
|
21
|
-
as: :item
|
22
|
-
end
|
23
|
-
|
24
17
|
def accordion_actions
|
25
18
|
tag.div role: "toolbar", data: { tree_accordion_controls: "" } do
|
26
19
|
concat tag.span(role: "button", value: "collapse",
|
@@ -6,6 +6,7 @@ module Katalyst
|
|
6
6
|
class Menu < Base
|
7
7
|
ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
|
8
8
|
submit->#{MENU_CONTROLLER}#reindex
|
9
|
+
navigation:drop->#{MENU_CONTROLLER}#drop
|
9
10
|
navigation:reindex->#{MENU_CONTROLLER}#reindex
|
10
11
|
navigation:reset->#{MENU_CONTROLLER}#reset
|
11
12
|
ACTIONS
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Katalyst
|
4
4
|
module Navigation
|
5
|
-
# STI base class for menu items (links and buttons)
|
5
|
+
# STI base class for menu items (headings, links and buttons)
|
6
6
|
class Item < ApplicationRecord
|
7
7
|
belongs_to :menu, inverse_of: :items, class_name: "Katalyst::Navigation::Menu"
|
8
8
|
|
@@ -10,6 +10,10 @@ module Katalyst
|
|
10
10
|
|
11
11
|
attr_accessor :parent, :children, :index, :depth
|
12
12
|
|
13
|
+
def layout?
|
14
|
+
is_a? Layout
|
15
|
+
end
|
16
|
+
|
13
17
|
private
|
14
18
|
|
15
19
|
def initialize_tree
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<%= form_with model: item, scope: :item, url: path do |form| %>
|
2
|
+
<%= form.hidden_field :type %>
|
3
|
+
|
4
|
+
<div class="field">
|
5
|
+
<%= form.label :title %>
|
6
|
+
<%= form.text_field :title %>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<div class="field">
|
10
|
+
<%= form.label :visible %>
|
11
|
+
<%= form.check_box :visible %>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<%= form.submit "Done" %>
|
15
|
+
<%= link_to "Discard", item.menu %>
|
16
|
+
<% end %>
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<div class="tree" data-invisible="<%= !item.visible? %>">
|
3
3
|
<%= builder.accordion_actions %>
|
4
4
|
|
5
|
-
<span role="img" value="<%= item.url.present? ? "link" : "title" %>" title="
|
5
|
+
<span role="img" value="<%= item.url.present? ? "link" : "title" %>" title="Type"></span>
|
6
6
|
<h4 class="title" title="<%= item.title %>"><%= item.title %></h4>
|
7
7
|
<span role="img" value="invisible" title="Hidden"></span>
|
8
8
|
</div>
|
@@ -11,4 +11,8 @@ FactoryBot.define do
|
|
11
11
|
url { Faker::Internet.unique.url }
|
12
12
|
http_method { Katalyst::Navigation::Button::HTTP_METHODS.keys.sample }
|
13
13
|
end
|
14
|
+
|
15
|
+
factory :katalyst_navigation_heading, aliases: [:navigation_heading], class: "Katalyst::Navigation::Heading" do
|
16
|
+
title { Faker::Beer.hop }
|
17
|
+
end
|
14
18
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: katalyst-navigation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Katalyst Interactive
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -50,12 +50,15 @@ files:
|
|
50
50
|
- app/models/concerns/katalyst/navigation/garbage_collection.rb
|
51
51
|
- app/models/concerns/katalyst/navigation/has_tree.rb
|
52
52
|
- app/models/katalyst/navigation/button.rb
|
53
|
+
- app/models/katalyst/navigation/heading.rb
|
53
54
|
- app/models/katalyst/navigation/item.rb
|
55
|
+
- app/models/katalyst/navigation/layout.rb
|
54
56
|
- app/models/katalyst/navigation/link.rb
|
55
57
|
- app/models/katalyst/navigation/menu.rb
|
56
58
|
- app/models/katalyst/navigation/node.rb
|
57
59
|
- app/models/katalyst/navigation/types/nodes_type.rb
|
58
60
|
- app/views/katalyst/navigation/items/_button.html.erb
|
61
|
+
- app/views/katalyst/navigation/items/_heading.html.erb
|
59
62
|
- app/views/katalyst/navigation/items/_link.html.erb
|
60
63
|
- app/views/katalyst/navigation/items/edit.html.erb
|
61
64
|
- app/views/katalyst/navigation/items/new.html.erb
|