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
@@ -0,0 +1,140 @@
1
+ export default class RulesEngine {
2
+ static rules = [
3
+ "denyDeNest",
4
+ "denyNest",
5
+ "denyCollapse",
6
+ "denyExpand",
7
+ "denyRemove",
8
+ "denyDrag",
9
+ "denyEdit",
10
+ "invalidDepth",
11
+ ];
12
+
13
+ /**
14
+ * Apply rules to the given item by computing a ruleset then merging it
15
+ * with the item's current state.
16
+ *
17
+ * @param {Item} item
18
+ */
19
+ update(item) {
20
+ this.rules = {};
21
+
22
+ this.firstItemDepthZero(item);
23
+ this.depthMustBeSet(item);
24
+ this.parentsCannotDeNest(item);
25
+ this.rootsCannotDeNest(item);
26
+ this.nestingNeedsParent(item);
27
+ this.leavesCannotCollapse(item);
28
+ this.needHiddenItemsToExpand(item);
29
+ this.parentsCannotBeDeleted(item);
30
+ this.parentsCannotBeDragged(item);
31
+ this.itemCannotHaveInvalidDepth(item);
32
+
33
+ RulesEngine.rules.forEach((rule) => {
34
+ item.toggleRule(rule, !!this.rules[rule]);
35
+ });
36
+ }
37
+
38
+ /**
39
+ * First item can't have a parent, so its depth should always be 0
40
+ */
41
+ firstItemDepthZero(item) {
42
+ if (item.index === 0) {
43
+ item.depth = 0;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Every item should have a non-negative depth set.
49
+ *
50
+ * @param {Item} item
51
+ */
52
+ depthMustBeSet(item) {
53
+ if (isNaN(item.depth) || item.depth < 0) {
54
+ item.depth = 0;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * De-nesting an item would create a gap of 2 between itself and its children
60
+ *
61
+ * @param {Item} item
62
+ */
63
+ parentsCannotDeNest(item) {
64
+ if (item.hasExpandedDescendants()) this.#deny("denyDeNest");
65
+ }
66
+
67
+ /**
68
+ * Item depth can't go below 0.
69
+ *
70
+ * @param {Item} item
71
+ */
72
+ rootsCannotDeNest(item) {
73
+ if (item.depth === 0) this.#deny("denyDeNest");
74
+ }
75
+
76
+ /**
77
+ * If an item doesn't have children it can't be collapsed.
78
+ *
79
+ * @param {Item} item
80
+ */
81
+ leavesCannotCollapse(item) {
82
+ if (!item.hasExpandedDescendants()) this.#deny("denyCollapse");
83
+ }
84
+
85
+ /**
86
+ * If an item doesn't have any hidden descendants then it can't be expanded.
87
+ *
88
+ * @param {Item} item
89
+ */
90
+ needHiddenItemsToExpand(item) {
91
+ if (!item.hasCollapsedDescendants()) this.#deny("denyExpand");
92
+ }
93
+
94
+ /**
95
+ * An item can't be nested (indented) if it doesn't have a valid parent.
96
+ *
97
+ * @param {Item} item
98
+ */
99
+ nestingNeedsParent(item) {
100
+ const previous = item.previousItem;
101
+ if (!previous || previous.depth < item.depth) this.#deny("denyNest");
102
+ }
103
+
104
+ /**
105
+ * An item can't be deleted if it has visible children.
106
+ *
107
+ * @param {Item} item
108
+ */
109
+ parentsCannotBeDeleted(item) {
110
+ if (item.hasExpandedDescendants()) this.#deny("denyRemove");
111
+ }
112
+
113
+ /**
114
+ * Items cannot be dragged if they have visible children.
115
+ *
116
+ * @param {Item} item
117
+ */
118
+ parentsCannotBeDragged(item) {
119
+ if (item.hasExpandedDescendants()) this.#deny("denyDrag");
120
+ }
121
+
122
+ /**
123
+ * Depth must increase stepwise.
124
+ *
125
+ * @param {Item} item
126
+ */
127
+ itemCannotHaveInvalidDepth(item) {
128
+ const previous = item.previousItem;
129
+ if (previous && previous.depth < item.depth - 1) this.#deny("invalidDepth");
130
+ }
131
+
132
+ /**
133
+ * Record a deny.
134
+ *
135
+ * @param rule {String}
136
+ */
137
+ #deny(rule) {
138
+ this.rules[rule] = true;
139
+ }
140
+ }
@@ -0,0 +1,17 @@
1
+ %icon-block {
2
+ display: block;
3
+ cursor: pointer;
4
+ position: relative;
5
+ padding: 0.65rem;
6
+ min-width: 2.5rem;
7
+ min-height: 2.5rem;
8
+ }
9
+
10
+ %icon {
11
+ position: absolute;
12
+ content: "";
13
+ width: 1.2rem;
14
+ height: 1.2rem;
15
+ background-repeat: no-repeat;
16
+ background-position: center;
17
+ }
@@ -0,0 +1,145 @@
1
+ @use "icon";
2
+
3
+ @use "item-actions";
4
+ @use "item-rules";
5
+ @use "new-items";
6
+ @use "status-bar";
7
+
8
+ $grey-light: #f4f4f4 !default;
9
+ $grey: #ececec !default;
10
+ $grey-dark: #999 !default;
11
+ $table-hover-background: #fff0eb !default;
12
+ $primary-color: #ff521f !default;
13
+
14
+ $row-inset: 2rem !default;
15
+ $row-height: 3rem !default;
16
+
17
+ $table-header-color: $grey !default;
18
+ $row-background-color: $grey-light !default;
19
+ $row-hover-color: $table-hover-background !default;
20
+ $icon-active-color: $primary-color !default;
21
+ $icon-passive-color: $grey-dark !default;
22
+
23
+ $status-published-background-color: #ebf9eb !default;
24
+ $status-published-border-color: #4dd45c !default;
25
+ $status-published-color: #4dd45c !default;
26
+
27
+ $status-draft-background-color: #fefaf3 !default;
28
+ $status-draft-border-color: #ffa800 !default;
29
+ $status-draft-color: #ffa800 !default;
30
+
31
+ $status-dirty-background-color: #eee !default;
32
+ $status-dirty-border-color: #888 !default;
33
+ $status-dirty-color: #aaa !default;
34
+
35
+ [data-controller="navigation--editor--menu"] {
36
+ --row-height: #{$row-height};
37
+ --row-inset: #{$row-inset};
38
+ --table-header-color: #{$table-header-color};
39
+ --row-background-color: #{$row-background-color};
40
+ --row-hover-color: #{$row-hover-color};
41
+ --icon-active-color: #{$icon-active-color};
42
+ --icon-passive-color: #{$icon-passive-color};
43
+
44
+ ol,
45
+ li {
46
+ margin: 0;
47
+ padding: 0;
48
+ padding-inline-start: 0;
49
+ list-style: none;
50
+ }
51
+
52
+ .hidden {
53
+ display: none !important;
54
+ }
55
+ }
56
+
57
+ [data-controller="navigation--editor--list"] {
58
+ min-height: var(--row-height);
59
+
60
+ // tree items
61
+ & > li {
62
+ display: block;
63
+ min-height: var(--row-height);
64
+
65
+ // https://github.com/react-dnd/react-dnd/issues/832
66
+ transform: translate3d(0, 0, 0);
67
+
68
+ // Pinstripe effect
69
+ &:nth-of-type(even) {
70
+ background: var(--row-background-color);
71
+ }
72
+
73
+ &:hover {
74
+ background: var(--row-hover-color);
75
+ }
76
+
77
+ &[draggable] {
78
+ cursor: grab;
79
+ }
80
+
81
+ // Dragged visuals
82
+ &[data-dragging] {
83
+ border: 2px dashed;
84
+
85
+ > * {
86
+ visibility: hidden;
87
+ }
88
+ }
89
+
90
+ // Depth spacing
91
+ @for $i from 1 through 6 {
92
+ &[data-navigation-depth="#{$i}"] .tree {
93
+ padding-left: calc(var(--row-inset) * #{$i});
94
+ }
95
+ }
96
+
97
+ .tree {
98
+ display: flex;
99
+ align-items: center;
100
+ }
101
+
102
+ .title,
103
+ .url {
104
+ text-overflow: ellipsis;
105
+ overflow: hidden;
106
+ white-space: nowrap;
107
+ }
108
+ }
109
+ }
110
+
111
+ [data-controller="navigation--editor--menu"] [role="rowheader"],
112
+ [data-controller="navigation--editor--item"] {
113
+ display: grid;
114
+ grid-template-columns: 40% 2fr auto;
115
+ padding: 0.25rem 0.5rem;
116
+ gap: 1rem;
117
+ align-items: center;
118
+ }
119
+
120
+ // Ensures vertical alignment of header with rows
121
+ [data-controller="navigation--editor-menu"] {
122
+ [role="rowheader"] {
123
+ min-height: var(--row-height);
124
+ background: var(--table-header-color);
125
+ padding-inline: 1.25rem 1rem;
126
+ }
127
+ }
128
+
129
+ [data-controller="navigation--editor--status-bar"] {
130
+ --background: #{$status-published-background-color};
131
+ --color: #{$status-published-border-color};
132
+ --border: #{$status-published-color};
133
+
134
+ &[data-state="draft"] {
135
+ --background: #{$status-draft-background-color};
136
+ --color: #{$status-draft-border-color};
137
+ --border: #{$status-draft-color};
138
+ }
139
+
140
+ &[data-state="dirty"] {
141
+ --background: #{$status-dirty-background-color};
142
+ --color: #{$status-dirty-border-color};
143
+ --border: #{$status-dirty-color};
144
+ }
145
+ }
@@ -0,0 +1,92 @@
1
+ @use "icon" as *;
2
+
3
+ [data-controller="navigation--editor--item"] {
4
+ [data-tree-accordion-controls] {
5
+ min-width: 2.5rem;
6
+ min-height: 2.5rem;
7
+ display: grid;
8
+ }
9
+
10
+ [data-tree-controls] {
11
+ display: flex;
12
+ align-items: center;
13
+ }
14
+
15
+ [data-tree-accordion-controls],
16
+ [data-tree-controls] {
17
+ [role="button"] {
18
+ @extend %icon-block;
19
+ &::before {
20
+ @extend %icon;
21
+ }
22
+ }
23
+ }
24
+
25
+ [role="img"][value="link"],
26
+ [role="img"][value="title"] {
27
+ width: 1.5rem;
28
+ height: 1.5rem;
29
+ display: grid;
30
+ place-items: center;
31
+ border-radius: 2px;
32
+ background: var(--icon-active-color);
33
+ margin-right: 0.5rem;
34
+
35
+ [data-invisible="true"] & {
36
+ background: var(--icon-passive-color);
37
+ }
38
+
39
+ &::before {
40
+ @extend %icon;
41
+ color: white;
42
+ font-size: 1.125rem;
43
+ line-height: 1.125rem;
44
+ text-align: center;
45
+ }
46
+
47
+ &[value="link"] {
48
+ &::before {
49
+ content: "#";
50
+ }
51
+ }
52
+
53
+ &[value="title"] {
54
+ &::before {
55
+ content: "T";
56
+ }
57
+ }
58
+ }
59
+
60
+ [role="img"][value="invisible"] {
61
+ @extend %icon-block;
62
+
63
+ &::before {
64
+ @extend %icon;
65
+ background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_58_2189)'%3E%3Cpath d='M22.1699 0.329991C21.7304 -0.109509 21.0179 -0.109509 20.5784 0.329991L15.8399 5.06849C14.6219 4.69949 13.3334 4.50149 11.9984 4.50149C6.76494 4.50149 2.22744 7.54949 -0.00156052 12.0015C0.962939 13.926 2.35794 15.588 4.05294 16.8555L0.326939 20.5815C-0.112561 21.021 -0.112561 21.7335 0.326939 22.173C0.545939 22.392 0.833939 22.503 1.12194 22.503C1.40994 22.503 1.69794 22.3935 1.91694 22.173L22.1669 1.92299C22.6064 1.48349 22.6064 0.770991 22.1669 0.331491L22.1699 0.329991ZM9.74994 7.49999C10.7399 7.49999 11.5799 8.13899 11.8814 9.02849L9.02844 11.8815C8.14044 11.58 7.49994 10.74 7.49994 9.74999C7.49994 8.50799 8.50794 7.49999 9.74994 7.49999ZM2.58144 12C3.47844 10.581 4.67394 9.37649 6.08394 8.47799C6.17544 8.41949 6.26844 8.36249 6.36144 8.30699C6.12744 8.94749 5.99994 9.63899 5.99994 10.3605C5.99994 11.6475 6.40494 12.8385 7.09494 13.815L5.66694 15.243C4.43844 14.379 3.38844 13.2765 2.58144 12V12Z' fill='%2358607A'/%3E%3Cpath d='M18.0001 10.3589C18.0001 9.72295 17.9011 9.10945 17.7166 8.53345L10.1746 16.0754C10.7506 16.2599 11.3641 16.3589 12.0001 16.3589C15.3136 16.3589 18.0001 13.6724 18.0001 10.3589V10.3589Z' fill='%2358607A'/%3E%3Cpath d='M19.4536 6.79651L17.8276 8.42251C17.8576 8.44051 17.8876 8.45851 17.9161 8.47801C19.3261 9.37801 20.5216 10.5825 21.4186 12C20.5216 13.419 19.3261 14.6235 17.9161 15.522C16.1446 16.6515 14.0986 17.25 12.0001 17.25C11.0941 17.25 10.1971 17.139 9.3286 16.9215L7.5271 18.723C8.9266 19.2255 10.4326 19.5 12.0001 19.5C17.2336 19.5 21.7711 16.452 24.0001 12C22.9456 9.89251 21.3721 8.10001 19.4536 6.79651V6.79651Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_58_2189'%3E%3Crect width='24' height='24' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
66
+ }
67
+ }
68
+
69
+ [role="button"][value="collapse"]::before {
70
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M15 8.33333L13.825 7.15833L10 10.975L6.175 7.15833L5 8.33333L10 13.3333L15 8.33333Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white' transform='translate(20) rotate(90)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
71
+ }
72
+
73
+ [role="button"][value="expand"]::before {
74
+ background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M8.3332 5L7.1582 6.175L10.9749 10L7.1582 13.825L8.3332 15L13.3332 10L8.3332 5Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
75
+ }
76
+
77
+ [role="button"][value="de-nest"]::before {
78
+ background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M11.6668 15L12.8418 13.825L9.02513 10L12.8418 6.175L11.6668 5L6.6668 10L11.6668 15Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white' transform='translate(20 20) rotate(180)'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
79
+ }
80
+
81
+ [role="button"][value="nest"]::before {
82
+ background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1454)'%3E%3Cpath d='M8.3332 5L7.1582 6.175L10.9749 10L7.1582 13.825L8.3332 15L13.3332 10L8.3332 5Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1454'%3E%3Crect width='20' height='20' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
83
+ }
84
+
85
+ [role="button"][value="edit"]::before {
86
+ background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1406)'%3E%3Cpath d='M2.25 12.9375V15.75H5.0625L13.3575 7.45504L10.545 4.64254L2.25 12.9375ZM15.5325 5.28004C15.825 4.98754 15.825 4.51504 15.5325 4.22254L13.7775 2.46754C13.485 2.17504 13.0125 2.17504 12.72 2.46754L11.3475 3.84004L14.16 6.65254L15.5325 5.28004V5.28004Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1406'%3E%3Crect width='18' height='18' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
87
+ }
88
+
89
+ [role="button"][value="remove"]::before {
90
+ background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_50_1409)'%3E%3Cpath d='M4.5 14.25C4.5 15.075 5.175 15.75 6 15.75H12C12.825 15.75 13.5 15.075 13.5 14.25V5.25H4.5V14.25ZM14.25 3H11.625L10.875 2.25H7.125L6.375 3H3.75V4.5H14.25V3Z' fill='%2358607A'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='clip0_50_1409'%3E%3Crect width='18' height='18' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E");
91
+ }
92
+ }
@@ -0,0 +1,24 @@
1
+ [data-controller="navigation--editor--list"] {
2
+ // Lower opacity for buttons you can't use
3
+ [data-deny-de-nest] [role="button"][value="de-nest"],
4
+ [data-deny-nest] [role="button"][value="nest"],
5
+ [data-deny-remove] [role="button"][value="remove"],
6
+ [data-deny-drag] [role="button"][value="drag"],
7
+ [data-deny-edit] [role="button"][value="edit"] {
8
+ opacity: 0.2;
9
+ pointer-events: none;
10
+ }
11
+
12
+ // Only show 1 of the collapse / expand button
13
+ [data-deny-collapse] [role="button"][value="collapse"],
14
+ [data-deny-expand] [role="button"][value="expand"],
15
+ [data-invisible="false"] [role="img"][value="invisible"] {
16
+ display: none !important;
17
+ pointer-events: none;
18
+ }
19
+
20
+ [data-invalid-depth] {
21
+ border-color: red;
22
+ background-color: indianred;
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ @use "icon" as *;
2
+
3
+ .navigation--editor--new-items {
4
+ display: grid;
5
+ grid-template-columns: repeat(3, 1fr);
6
+ gap: 0.25rem;
7
+
8
+ [role="listitem"] {
9
+ display: flex;
10
+ flex-direction: column;
11
+ justify-content: center;
12
+ align-items: center;
13
+ transform: translate3d(0, 0, 0);
14
+ cursor: grab;
15
+
16
+ &::before {
17
+ @extend %icon;
18
+ position: unset;
19
+ background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16 3.66667V13.3333C16 14.8 14.6909 16 13.0909 16H7.78182C6.99636 16 6.25455 15.7133 5.70909 15.2067L0 9.88667C0 9.88667 0.916364 9.06667 0.945455 9.05333C1.10545 8.92667 1.30182 8.86 1.52 8.86C1.68 8.86 1.82545 8.9 1.95636 8.96667C1.98545 8.97333 5.09091 10.6067 5.09091 10.6067V2.66667C5.09091 2.11333 5.57818 1.66667 6.18182 1.66667C6.78545 1.66667 7.27273 2.11333 7.27273 2.66667V7.33333H8V1C8 0.446667 8.48727 0 9.09091 0C9.69455 0 10.1818 0.446667 10.1818 1V7.33333H10.9091V1.66667C10.9091 1.11333 11.3964 0.666667 12 0.666667C12.6036 0.666667 13.0909 1.11333 13.0909 1.66667V7.33333H13.8182V3.66667C13.8182 3.11333 14.3055 2.66667 14.9091 2.66667C15.5127 2.66667 16 3.11333 16 3.66667Z' fill='white'/%3E%3C/svg%3E");
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,87 @@
1
+ [data-controller="navigation--editor--status-bar"] {
2
+ min-height: 3rem;
3
+ line-height: 3rem;
4
+ padding: 0 1.25rem 0 1.5rem;
5
+ background: var(--background);
6
+ color: var(--color);
7
+ border: 1px solid var(--border);
8
+ margin-bottom: 1rem;
9
+
10
+ display: grid;
11
+ grid-template-columns: 1fr auto;
12
+ grid-template-areas: "status actions";
13
+ align-items: baseline;
14
+ grid-column-gap: 2rem;
15
+
16
+ .status-text {
17
+ display: none;
18
+ grid-area: status;
19
+ font-weight: bold;
20
+ }
21
+
22
+ &[data-state="published"] .status-text[data-published],
23
+ &[data-state="draft"] .status-text[data-draft],
24
+ &[data-state="dirty"] .status-text[data-dirty] {
25
+ display: unset;
26
+ }
27
+
28
+ menu {
29
+ display: inline;
30
+ grid-area: actions;
31
+ margin: 0;
32
+ padding: 0;
33
+ }
34
+
35
+ menu > li {
36
+ display: inline;
37
+ }
38
+
39
+ .button {
40
+ color: inherit;
41
+ line-height: 1rem;
42
+ margin-left: 0.5rem;
43
+ }
44
+
45
+ .button--primary {
46
+ background: var(--color);
47
+ border: 1px solid var(--border);
48
+ color: white;
49
+
50
+ &[disabled] {
51
+ background: var(--color);
52
+ opacity: 0.8;
53
+ }
54
+ }
55
+
56
+ .button--secondary {
57
+ background: var(--background);
58
+ border: 1px solid var(--border);
59
+ color: var(--color);
60
+
61
+ &[disabled] {
62
+ background: var(--background);
63
+ }
64
+ }
65
+
66
+ &[data-state="published"] {
67
+ [value="publish"],
68
+ [value="save"],
69
+ [value="revert"],
70
+ [value="discard"] {
71
+ display: none;
72
+ }
73
+ }
74
+
75
+ &[data-state="draft"] {
76
+ [value="save"],
77
+ [value="discard"] {
78
+ display: none;
79
+ }
80
+ }
81
+
82
+ &[data-state="dirty"] {
83
+ [value="revert"] {
84
+ display: none;
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ class BaseController < ActionController::Base
6
+ include Katalyst::Tables::Backend
7
+
8
+ helper Katalyst::Navigation::EditorHelper
9
+ helper Katalyst::Tables::Frontend
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ class ItemsController < BaseController
6
+ before_action :set_menu
7
+ before_action :set_item, except: %i[new create]
8
+
9
+ def new
10
+ render locals: { item: @menu.items.build(type: new_item_params) }
11
+ end
12
+
13
+ def create
14
+ item = @menu.items.build(item_params)
15
+ if item.save
16
+ render :update, locals: { item: item, previous: @menu.items.build(type: item.type) }
17
+ else
18
+ render :new, status: :unprocessable_entity, locals: { item: item }
19
+ end
20
+ end
21
+
22
+ def edit
23
+ render locals: { item: @item }
24
+ end
25
+
26
+ def update
27
+ @item.attributes = item_params
28
+
29
+ if @item.valid?
30
+ previous = @item
31
+ @item = @item.dup.tap(&:save!)
32
+ render locals: { item: @item, previous: previous }
33
+ else
34
+ render :edit, status: :unprocessable_entity, locals: { item: @item }
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def new_item_params
41
+ params[:type] || Link.name
42
+ end
43
+
44
+ def item_params
45
+ params.require(:item).permit(%i[title url visible http_method new_tab type])
46
+ end
47
+
48
+ def set_menu
49
+ @menu = Menu.find(params[:menu_id])
50
+ end
51
+
52
+ def set_item
53
+ @item = @menu.items.find(params[:id])
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ class MenusController < BaseController
6
+ def index
7
+ sort, menus = table_sort(Menu.all)
8
+
9
+ render locals: { menus: menus, sort: sort }
10
+ end
11
+
12
+ def new
13
+ render locals: { menu: Menu.new }
14
+ end
15
+
16
+ def create
17
+ @menu = Menu.new(menu_params)
18
+
19
+ if @menu.save
20
+ redirect_to @menu
21
+ else
22
+ render :new, locals: { menu: @menu }, status: :unprocessable_entity
23
+ end
24
+ end
25
+
26
+ def show
27
+ menu = Menu.find(params[:id])
28
+
29
+ render locals: { menu: menu }
30
+ end
31
+
32
+ def edit
33
+ menu = Menu.find(params[:id])
34
+
35
+ render locals: { menu: menu }
36
+ end
37
+
38
+ # PATCH /admins/navigation_menus/:slug
39
+ def update
40
+ menu = Menu.find(params[:id])
41
+
42
+ menu.attributes = navigation_params
43
+
44
+ unless menu.valid?
45
+ return render :show, locals: { menu: menu }, status: :unprocessable_entity
46
+ end
47
+
48
+ case params[:commit]
49
+ when "publish"
50
+ menu.save!
51
+ menu.publish!
52
+ when "save"
53
+ menu.save!
54
+ when "revert"
55
+ menu.revert!
56
+ end
57
+ redirect_to menu
58
+ end
59
+
60
+ def destroy
61
+ menu = Menu.find(params[:id])
62
+
63
+ menu.destroy!
64
+
65
+ redirect_to action: :index
66
+ end
67
+
68
+ private
69
+
70
+ def menu_params
71
+ params.require(:menu).permit(:title, :slug)
72
+ end
73
+
74
+ def navigation_params
75
+ return {} if params[:menu].blank?
76
+
77
+ params.require(:menu)
78
+ .permit(:title, :slug, items_attributes: %i[id index depth])
79
+ end
80
+ end
81
+ end
82
+ end