katalyst-navigation 1.0.3 → 1.1.0

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: 94de0bbf95ab99d4893ec9decb52fa6c1a7231f1d58d17b1263e63124baaf10a
4
- data.tar.gz: 3ec009c2845a80e4371aca788ac2b06de9a602cc468afecbbb5666d5a50fcb07
3
+ metadata.gz: 94cc7f739764b2d65865d91d0d6f1431671bee909e285b51238f4343dd15883e
4
+ data.tar.gz: 51a2493050682848606784167c351865776daa9cc403e59e096e269b55afc208
5
5
  SHA512:
6
- metadata.gz: 043ce598613d8f99b98d777be94b0805d1504989eab5dba1e651a8d7fa2a2d8d4f89135530e300e65d37fea900237cba0e2a30caa7e95e81ab17000639bba5bb
7
- data.tar.gz: c0795c1f30ed8ecea198e90d90097bcb6728bcbe2e7116ecd106e0c21c6e97e9c68a4ff981586bfa9436a22a7e63adf8c33da00f593541b17e77d316675fe4bb
6
+ metadata.gz: 51127826086e93bdc9d6796f8eab5096c0d4a1d6af14a0623f3387a6c567263b6f28351507f5bcb683cce7f88f0a5832ac7583e2a28d59b31463eeeba8b09710
7
+ data.tar.gz: 1b7b2e4b291da7ae044b6c5de3c5df0a0d56b5ba111d7fbf454648916baf2a8634cfbc2c94216a3f327ad5ffba51440b00ca5d052bcf0d55b070b345de2a72ea
@@ -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();
@@ -9,6 +9,10 @@ export default class RulesEngine {
9
9
  "denyEdit",
10
10
  ];
11
11
 
12
+ constructor(maxDepth = null) {
13
+ this.maxDepth = maxDepth;
14
+ }
15
+
12
16
  /**
13
17
  * Apply rules to the given item by computing a ruleset then merging it
14
18
  * with the item's current state.
@@ -22,11 +26,13 @@ export default class RulesEngine {
22
26
  this.firstItemDepthZero(item);
23
27
  this.depthMustBeSet(item);
24
28
  this.itemCannotHaveInvalidDepth(item);
29
+ this.itemCannotExceedDepthLimit(item);
25
30
 
26
31
  // behavioural rules define what the user is allowed to do
27
32
  this.parentsCannotDeNest(item);
28
33
  this.rootsCannotDeNest(item);
29
34
  this.nestingNeedsParent(item);
35
+ this.nestingCannotExceedMaxDepth(item);
30
36
  this.leavesCannotCollapse(item);
31
37
  this.needHiddenItemsToExpand(item);
32
38
  this.parentsCannotBeDeleted(item);
@@ -69,6 +75,22 @@ export default class RulesEngine {
69
75
  }
70
76
  }
71
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
+
72
94
  /**
73
95
  * De-nesting an item would create a gap of 2 between itself and its children
74
96
  *
@@ -115,6 +137,17 @@ export default class RulesEngine {
115
137
  if (!previous || previous.depth < item.depth) this.#deny("denyNest");
116
138
  }
117
139
 
140
+ /**
141
+ * An item can't be nested (indented) if doing so would exceed the max depth.
142
+ *
143
+ * @param {Item} item
144
+ */
145
+ nestingCannotExceedMaxDepth(item) {
146
+ if (this.maxDepth > 0 && this.maxDepth >= item.depth + 1) {
147
+ this.#deny("denyNest");
148
+ }
149
+ }
150
+
118
151
  /**
119
152
  * An item can't be deleted if it has visible children.
120
153
  *
@@ -2,20 +2,87 @@
2
2
 
3
3
  module Katalyst
4
4
  module Navigation
5
+ # Preloads navigation menus before content is rendered.
5
6
  module HasNavigation
6
7
  extend ActiveSupport::Concern
7
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
8
56
  module NavigationHelper
57
+ # Retrieves the preloaded menu that matches the given slug.
58
+ #
59
+ # @return {Katalyst::Navigation::Menu} menu with the given slug
9
60
  def navigation_menu_for(slug)
10
61
  @navigation_menus[slug.to_s]
11
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
12
72
  end
13
73
 
14
74
  included do
75
+ include NavigationBuilder
76
+
15
77
  helper Katalyst::Navigation::FrontendHelper
16
78
  helper NavigationHelper
17
79
 
18
- before_action :set_navigation_menus
80
+ # @see ActionController::Rendering#render
81
+ def render(*args)
82
+ set_navigation_menus
83
+
84
+ super
85
+ end
19
86
  end
20
87
 
21
88
  protected
@@ -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
@@ -68,14 +68,9 @@ module Katalyst
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,16 +1,21 @@
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
+ <div class="field optional">
15
+ <%= form.label :depth %>
16
+ <%= form.text_field :depth %>
17
+ </div>
18
+
14
19
  <div class="button-row">
15
20
  <%= form.submit :save %>
16
21
  <%= link_to "Delete", url_for(action: :show),
@@ -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.3"
5
+ VERSION = "1.1.0"
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.3
4
+ version: 1.1.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-09-07 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -73,6 +73,7 @@ files:
73
73
  - config/routes.rb
74
74
  - db/migrate/20220826034057_create_katalyst_navigation_menus.rb
75
75
  - db/migrate/20220826034507_create_katalyst_navigation_items.rb
76
+ - db/migrate/20220908044500_add_depth_limit_to_menus.rb
76
77
  - lib/katalyst/navigation.rb
77
78
  - lib/katalyst/navigation/engine.rb
78
79
  - lib/katalyst/navigation/version.rb