katalyst-navigation 1.0.2 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b1e30c9f62f9c95fbd95824c1fca798f29e8d8240040cf92bfe3e6d802f0f4b
4
- data.tar.gz: 7b86b9670845b41baa6214bd1ac19e5b6cf9aed0e391f31f270b183b5891e32a
3
+ metadata.gz: 5c41babca4e58e13c18e53437a8992f93a1f2e82566542ed43dd8f62a2a0a83f
4
+ data.tar.gz: 4d3ea6600b660cf877ac8c53aa81be4367169ea27ffca16d86374ca0a61a0b41
5
5
  SHA512:
6
- metadata.gz: 247b24aec0411a6e45c316a68d1020f57bb3672cb2c7f649f3deab98c6c0c3820b3ca49fd4074b779d693ee4bc39ecaad112586b2a75fc32cddb574be8568245
7
- data.tar.gz: 8dbe6529fea49f7f497ee5d1310cc5f4f618f86f41bfcd9b83395a91bf4c5764e9fe7ddab32bd0da04826689c5f5b6556ee3bfda79da6ee48397db9e733aa70f
6
+ metadata.gz: dfff1369051123cbd3411fe4d873a8a1bee97166c09f05c117f6df9a11b0a0b618d8255349aed78213b76ee5b8c078807d904fd51899e803e2222f56d9beb086
7
+ data.tar.gz: 07e367633bd15cc6f84aa242d5f785e82822f9354482ed071a90e4ed975aa4ed40eeac22bea5d30ab949031d7aa6702cfd18c7fae6f3f9beb08d2c202dbd6d1d
@@ -6,6 +6,9 @@ import RulesEngine from "utils/navigation/editor/rules-engine";
6
6
 
7
7
  export default class MenuController extends Controller {
8
8
  static targets = ["menu"];
9
+ static values = {
10
+ maxDepth: Number,
11
+ };
9
12
 
10
13
  connect() {
11
14
  this.state = this.menu.state;
@@ -85,7 +88,7 @@ export default class MenuController extends Controller {
85
88
  if (!this.updateRequested) return;
86
89
 
87
90
  this.updateRequested = false;
88
- const engine = new RulesEngine();
91
+ const engine = new RulesEngine(this.maxDepthValue);
89
92
  this.menu.items.forEach((item) => engine.update(item));
90
93
 
91
94
  this.#notifyChange();
@@ -7,9 +7,12 @@ export default class RulesEngine {
7
7
  "denyRemove",
8
8
  "denyDrag",
9
9
  "denyEdit",
10
- "invalidDepth",
11
10
  ];
12
11
 
12
+ constructor(maxDepth = null) {
13
+ this.maxDepth = maxDepth;
14
+ }
15
+
13
16
  /**
14
17
  * Apply rules to the given item by computing a ruleset then merging it
15
18
  * with the item's current state.
@@ -19,16 +22,21 @@ export default class RulesEngine {
19
22
  update(item) {
20
23
  this.rules = {};
21
24
 
25
+ // structural rules enforce a valid tree structure
22
26
  this.firstItemDepthZero(item);
23
27
  this.depthMustBeSet(item);
28
+ this.itemCannotHaveInvalidDepth(item);
29
+ this.itemCannotExceedDepthLimit(item);
30
+
31
+ // behavioural rules define what the user is allowed to do
24
32
  this.parentsCannotDeNest(item);
25
33
  this.rootsCannotDeNest(item);
26
34
  this.nestingNeedsParent(item);
35
+ this.nestingCannotExceedMaxDepth(item);
27
36
  this.leavesCannotCollapse(item);
28
37
  this.needHiddenItemsToExpand(item);
29
38
  this.parentsCannotBeDeleted(item);
30
39
  this.parentsCannotBeDragged(item);
31
- this.itemCannotHaveInvalidDepth(item);
32
40
 
33
41
  RulesEngine.rules.forEach((rule) => {
34
42
  item.toggleRule(rule, !!this.rules[rule]);
@@ -55,6 +63,34 @@ export default class RulesEngine {
55
63
  }
56
64
  }
57
65
 
66
+ /**
67
+ * Depth must increase stepwise.
68
+ *
69
+ * @param {Item} item
70
+ */
71
+ itemCannotHaveInvalidDepth(item) {
72
+ const previous = item.previousItem;
73
+ if (previous && previous.depth < item.depth - 1) {
74
+ item.depth = previous.depth + 1;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Depth must not exceed menu's depth limit.
80
+ *
81
+ * @param {Item} item
82
+ */
83
+ itemCannotExceedDepthLimit(item) {
84
+ if (this.maxDepth > 0 && this.maxDepth <= item.depth) {
85
+ // Note: this change can cause an issue where the previous item is treated
86
+ // like a parent even though it no longer has children. This is because
87
+ // items are processed in order. This issue does not seem worth solving
88
+ // as it only occurs if the max depth is altered. The issue can be worked
89
+ // around by saving the menu.
90
+ item.depth = this.maxDepth - 1;
91
+ }
92
+ }
93
+
58
94
  /**
59
95
  * De-nesting an item would create a gap of 2 between itself and its children
60
96
  *
@@ -102,31 +138,32 @@ export default class RulesEngine {
102
138
  }
103
139
 
104
140
  /**
105
- * An item can't be deleted if it has visible children.
141
+ * An item can't be nested (indented) if doing so would exceed the max depth.
106
142
  *
107
143
  * @param {Item} item
108
144
  */
109
- parentsCannotBeDeleted(item) {
110
- if (item.hasExpandedDescendants()) this.#deny("denyRemove");
145
+ nestingCannotExceedMaxDepth(item) {
146
+ if (this.maxDepth > 0 && this.maxDepth <= item.depth + 1) {
147
+ this.#deny("denyNest");
148
+ }
111
149
  }
112
150
 
113
151
  /**
114
- * Items cannot be dragged if they have visible children.
152
+ * An item can't be deleted if it has visible children.
115
153
  *
116
154
  * @param {Item} item
117
155
  */
118
- parentsCannotBeDragged(item) {
119
- if (item.hasExpandedDescendants()) this.#deny("denyDrag");
156
+ parentsCannotBeDeleted(item) {
157
+ if (item.hasExpandedDescendants()) this.#deny("denyRemove");
120
158
  }
121
159
 
122
160
  /**
123
- * Depth must increase stepwise.
161
+ * Items cannot be dragged if they have visible children.
124
162
  *
125
163
  * @param {Item} item
126
164
  */
127
- itemCannotHaveInvalidDepth(item) {
128
- const previous = item.previousItem;
129
- if (previous && previous.depth < item.depth - 1) this.#deny("invalidDepth");
165
+ parentsCannotBeDragged(item) {
166
+ if (item.hasExpandedDescendants()) this.#deny("denyDrag");
130
167
  }
131
168
 
132
169
  /**
@@ -10,7 +10,6 @@ $grey: #ececec !default;
10
10
  $grey-dark: #999 !default;
11
11
  $table-hover-background: #fff0eb !default;
12
12
  $primary-color: #ff521f !default;
13
- $error-color: #990000 !default;
14
13
 
15
14
  $row-inset: 2rem !default;
16
15
  $row-height: 3rem !default;
@@ -20,7 +19,6 @@ $row-background-color: $grey-light !default;
20
19
  $row-hover-color: $table-hover-background !default;
21
20
  $icon-active-color: $primary-color !default;
22
21
  $icon-passive-color: $grey-dark !default;
23
- $row-depth-invalid-color: $error-color !default;
24
22
 
25
23
  $status-published-background-color: #ebf9eb !default;
26
24
  $status-published-border-color: #4dd45c !default;
@@ -42,7 +40,6 @@ $status-dirty-color: #aaa !default;
42
40
  --row-hover-color: #{$row-hover-color};
43
41
  --icon-active-color: #{$icon-active-color};
44
42
  --icon-passive-color: #{$icon-passive-color};
45
- --row-depth-invalid-color: #{$row-depth-invalid-color};
46
43
 
47
44
  ol,
48
45
  li {
@@ -16,8 +16,4 @@
16
16
  display: none !important;
17
17
  pointer-events: none;
18
18
  }
19
-
20
- [data-invalid-depth] {
21
- box-shadow: inset 0 0 0 3px var(--row-depth-invalid-color);
22
- }
23
19
  }
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Navigation
5
+ # Preloads navigation menus before content is rendered.
6
+ module HasNavigation
7
+ extend ActiveSupport::Concern
8
+
9
+ # Override the default navigation builder for all views rendered by this
10
+ # controller and any of its descendants. Accepts a subclass of
11
+ # Katalyst::Navigation::Frontend::Builder.
12
+ #
13
+ # For example, given a form builder:
14
+ #
15
+ # class AdminNavBuilder < Katalyst::Navigation::Frontend::Builder
16
+ # def render_item(item)
17
+ # end
18
+ # end
19
+ #
20
+ # The controller specifies a form builder as its default:
21
+ #
22
+ # class AdminAreaController < ApplicationController
23
+ # default_navigation_builder AdminNavBuilder
24
+ # end
25
+ #
26
+ # Then in the view any form using +navigation_menu_with+ will be an instance of the
27
+ # specified navigation builder:
28
+ #
29
+ # <%= navigation_menu_with(menu: @menu) %>
30
+ module NavigationBuilder
31
+ extend ActiveSupport::Concern
32
+
33
+ included do
34
+ class_attribute :_default_navigation_builder, instance_accessor: false
35
+ end
36
+
37
+ class_methods do
38
+ # Set the navigation builder to be used as the default for all navs
39
+ # in the views rendered by this controller and its subclasses.
40
+ #
41
+ # @param builder {Katalyst::Navigation::Frontend::Builder}
42
+ def default_navigation_builder(builder)
43
+ self._default_navigation_builder = builder
44
+ end
45
+ end
46
+
47
+ # Default navigation builder for the controller
48
+ #
49
+ # @return {Katalyst::Navigation::Frontend::Builder}
50
+ def default_navigation_builder
51
+ self.class._default_navigation_builder
52
+ end
53
+ end
54
+
55
+ # Provide an accessor for navigation menus
56
+ module NavigationHelper
57
+ # Retrieves the preloaded menu that matches the given slug.
58
+ #
59
+ # @return {Katalyst::Navigation::Menu} menu with the given slug
60
+ def navigation_menu_for(slug)
61
+ @navigation_menus[slug.to_s]
62
+ end
63
+
64
+ # @see ActionView::Helpers::ControllerHelper#assign_controller
65
+ def assign_controller(controller)
66
+ super
67
+
68
+ if controller.respond_to?(:default_navigation_builder)
69
+ @_default_navigation_builder = controller.default_navigation_builder
70
+ end
71
+ end
72
+ end
73
+
74
+ included do
75
+ include NavigationBuilder
76
+
77
+ helper Katalyst::Navigation::FrontendHelper
78
+ 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
+ end
93
+ end
94
+ end
95
+ end
@@ -39,7 +39,7 @@ module Katalyst
39
39
  def update
40
40
  menu = Menu.find(params[:id])
41
41
 
42
- menu.attributes = navigation_params
42
+ menu.attributes = menu_params
43
43
 
44
44
  unless menu.valid?
45
45
  return render :show, locals: { menu: menu }, status: :unprocessable_entity
@@ -62,20 +62,15 @@ module Katalyst
62
62
 
63
63
  menu.destroy!
64
64
 
65
- redirect_to action: :index
65
+ redirect_to action: :index, status: :see_other
66
66
  end
67
67
 
68
68
  private
69
69
 
70
70
  def menu_params
71
- params.require(:menu).permit(:title, :slug)
72
- end
73
-
74
- def navigation_params
75
71
  return {} if params[:menu].blank?
76
72
 
77
- params.require(:menu)
78
- .permit(:title, :slug, items_attributes: %i[id index depth])
73
+ params.require(:menu).permit(:title, :slug, :depth, items_attributes: %i[id index depth])
79
74
  end
80
75
  end
81
76
  end
@@ -8,7 +8,7 @@ module Katalyst
8
8
 
9
9
  def build(item, **options, &block)
10
10
  self.item = item
11
- content_tag(:div, default_options(id: dom_id(item), **options)) do
11
+ tag.div **default_options(id: dom_id(item), **options) do
12
12
  concat(capture { yield self }) if block
13
13
  concat fields(item)
14
14
  end
@@ -14,7 +14,7 @@ module Katalyst
14
14
  ACTIONS
15
15
 
16
16
  def build(options, &_block)
17
- content_tag :ol, default_options(id: menu_form_id, **options) do
17
+ tag.ol **default_options(id: menu_form_id, **options) do
18
18
  yield self
19
19
  end
20
20
  end
@@ -29,6 +29,10 @@ module Katalyst
29
29
  add_option(options, :data, :controller, MENU_CONTROLLER)
30
30
  add_option(options, :data, :action, ACTIONS)
31
31
 
32
+ depth = options.delete(:depth) || menu.depth
33
+
34
+ add_option(options, :data, :"#{MENU_CONTROLLER}-max-depth-value", depth) if depth
35
+
32
36
  options
33
37
  end
34
38
  end
@@ -10,7 +10,7 @@ module Katalyst
10
10
 
11
11
  def build(item, **options, &block)
12
12
  capture do
13
- concat(content_tag(:div, **default_options(options)) do
13
+ concat(tag.div(**default_options(options)) do
14
14
  concat capture(&block)
15
15
  concat item_template(item)
16
16
  end)
@@ -31,7 +31,7 @@ module Katalyst
31
31
  # events when the user initiates drag so that it can be copied into the
32
32
  # editor list on drop.
33
33
  def item_template(item)
34
- content_tag(:template, data: { "#{NEW_ITEM_CONTROLLER}-target" => "template" }) do
34
+ tag.template data: { "#{NEW_ITEM_CONTROLLER}-target" => "template" } do
35
35
  navigation_editor_items(item: item)
36
36
  end
37
37
  end
@@ -9,7 +9,7 @@ module Katalyst
9
9
  ACTIONS
10
10
 
11
11
  def build(**options)
12
- content_tag(:div, default_options(**options)) do
12
+ tag.div **default_options(**options) do
13
13
  concat status(:published, last_update: l(menu.updated_at, format: :short))
14
14
  concat status(:draft)
15
15
  concat status(:dirty)
@@ -24,7 +24,7 @@ module Katalyst
24
24
  end
25
25
 
26
26
  def actions
27
- content_tag(:menu) do
27
+ tag.menu do
28
28
  concat action(:discard, class: "button button--text")
29
29
  concat action(:revert, class: "button button--text") if menu.state == :draft
30
30
  concat action(:save, class: "button button--secondary")
@@ -8,15 +8,15 @@ module Katalyst
8
8
 
9
9
  delegate_missing_to :@template
10
10
 
11
- def initialize(template, menu: {}, list: {}, item: {})
11
+ def initialize(template, list: {}, item: {}, **menu_options)
12
12
  self.template = template
13
- self.menu_options = menu
13
+ self.menu_options = menu_options
14
14
  self.list_options = list
15
15
  self.item_options = item
16
16
  end
17
17
 
18
18
  def render(tree)
19
- content_tag(:ul, menu_options) do
19
+ tag.ul **menu_options do
20
20
  tree.each do |item|
21
21
  concat render_item(item)
22
22
  end
@@ -26,14 +26,14 @@ module Katalyst
26
26
  def render_item(item)
27
27
  return unless item.visible?
28
28
 
29
- content_tag :li, item_options do
29
+ tag.li **item_options do
30
30
  concat public_send("render_#{item.model_name.param_key}", item)
31
31
  concat render_list(item.children) if item.children.any?
32
32
  end
33
33
  end
34
34
 
35
35
  def render_list(items)
36
- content_tag :ul, list_options do
36
+ tag.ul **list_options do
37
37
  items.each do |child|
38
38
  concat render_item(child)
39
39
  end
@@ -5,15 +5,19 @@ module Katalyst
5
5
  module FrontendHelper
6
6
  mattr_accessor :navigation_builder
7
7
 
8
+ attr_internal :default_navigation_builder
9
+
8
10
  # Render a navigation menu. Caches based on the published version's id.
9
11
  #
10
12
  # @param(menu: Katalyst::Navigation::Menu)
11
13
  # @return Structured HTML containing top level + nested navigation links
12
- def render_navigation_menu(menu, item: {}, list: {}, **options)
14
+ def navigation_menu_with(menu:, **options)
15
+ builder = navigation_builder(**options)
16
+ menu = menu.is_a?(Symbol) ? navigation_menu_for(menu) : menu
17
+
13
18
  return unless menu&.published_version&.present?
14
19
 
15
20
  cache menu.published_version do
16
- builder = default_navigation_builder_class.new(self, menu: options, item: item, list: list)
17
21
  concat builder.render(menu.published_tree)
18
22
  end
19
23
  end
@@ -22,8 +26,9 @@ module Katalyst
22
26
  #
23
27
  # @param(items: [Katalyst::Navigation::Item])
24
28
  # @return Structured HTML containing top level + nested navigation links
25
- def render_navigation_items(items, list: {}, **options)
26
- builder = default_navigation_builder_class.new(self, item: options, list: list)
29
+ def navigation_items_with(items:, **options)
30
+ builder = navigation_builder(**options)
31
+
27
32
  capture do
28
33
  items.each do |item|
29
34
  concat builder.render_item(item)
@@ -33,8 +38,13 @@ module Katalyst
33
38
 
34
39
  private
35
40
 
41
+ def navigation_builder(**options)
42
+ builder = options.delete(:builder) || default_navigation_builder_class
43
+ builder.new(self, **options)
44
+ end
45
+
36
46
  def default_navigation_builder_class
37
- builder = controller.try(:default_navigation_builder) || Frontend::Builder
47
+ builder = default_navigation_builder || Frontend::Builder
38
48
  builder.respond_to?(:constantize) ? builder.constantize : builder
39
49
  end
40
50
  end
@@ -45,6 +45,7 @@ module Katalyst
45
45
 
46
46
  validates :title, :slug, presence: true
47
47
  validates :slug, uniqueness: true
48
+ validates :depth, numericality: { greater_than: 0, only_integer: true, allow_nil: true }
48
49
 
49
50
  # A menu is in draft mode if it has an unpublished draft or it has no published version.
50
51
  # @return the current state of the menu, either `published` or `draft`
@@ -1,15 +1,25 @@
1
1
  <% content_for :title, "Edit navigation" %>
2
2
 
3
3
  <%= form_with model: menu do |form| %>
4
- <div class="field">
4
+ <div class="field required">
5
5
  <%= form.label :title %>
6
6
  <%= form.text_field :title %>
7
7
  </div>
8
8
 
9
- <div class="field">
9
+ <div class="field required">
10
10
  <%= form.label :slug %>
11
11
  <%= form.text_field :slug %>
12
12
  </div>
13
13
 
14
- <%= form.submit :save %>
14
+ <div class="field optional">
15
+ <%= form.label :depth %>
16
+ <%= form.text_field :depth %>
17
+ </div>
18
+
19
+ <div class="button-row">
20
+ <%= form.submit :save %>
21
+ <%= link_to "Delete", url_for(action: :show),
22
+ class: "button button-secondary",
23
+ data: { turbo_method: :delete, turbo_confirm: "Are you sure?" } %>
24
+ </div>
15
25
  <% end %>
@@ -11,7 +11,4 @@
11
11
  <%= link_to cell.value, menu %>
12
12
  <% end %>
13
13
  <%= row.cell :slug %>
14
- <%= row.cell :actions do |cell| %>
15
- <%= button_to "Delete", menu, method: :delete, class: "button button--text" %>
16
- <% end %>
17
14
  <% end %>
@@ -1,15 +1,20 @@
1
1
  <% content_for :title, "New navigation" %>
2
2
 
3
3
  <%= form_with model: menu do |form| %>
4
- <div class="field">
4
+ <div class="field required">
5
5
  <%= form.label :title %>
6
6
  <%= form.text_field :title %>
7
7
  </div>
8
8
 
9
- <div class="field">
9
+ <div class="field required">
10
10
  <%= form.label :slug %>
11
11
  <%= form.text_field :slug %>
12
12
  </div>
13
13
 
14
+ <div class="field optional">
15
+ <%= form.label :depth %>
16
+ <%= form.text_field :depth %>
17
+ </div>
18
+
14
19
  <%= form.submit %>
15
20
  <% end %>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDepthLimitToMenus < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :katalyst_navigation_menus, :depth, :integer
6
+ end
7
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Katalyst
4
4
  module Navigation
5
- VERSION = "1.0.2"
5
+ VERSION = "1.1.1"
6
6
  end
7
7
  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.0.2
4
+ version: 1.1.1
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-09-06 00:00:00.000000000 Z
11
+ date: 2022-09-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -34,6 +34,7 @@ files:
34
34
  - app/assets/stylesheets/katalyst/navigation/editor/_item-rules.scss
35
35
  - app/assets/stylesheets/katalyst/navigation/editor/_new-items.scss
36
36
  - app/assets/stylesheets/katalyst/navigation/editor/_status-bar.scss
37
+ - app/controllers/concerns/katalyst/navigation/has_navigation.rb
37
38
  - app/controllers/katalyst/navigation/base_controller.rb
38
39
  - app/controllers/katalyst/navigation/items_controller.rb
39
40
  - app/controllers/katalyst/navigation/menus_controller.rb
@@ -72,6 +73,7 @@ files:
72
73
  - config/routes.rb
73
74
  - db/migrate/20220826034057_create_katalyst_navigation_menus.rb
74
75
  - db/migrate/20220826034507_create_katalyst_navigation_items.rb
76
+ - db/migrate/20220908044500_add_depth_limit_to_menus.rb
75
77
  - lib/katalyst/navigation.rb
76
78
  - lib/katalyst/navigation/engine.rb
77
79
  - lib/katalyst/navigation/version.rb