katalyst-content 2.7.0 → 2.8.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.
@@ -1,3 +1,20 @@
1
- <div class="content--editor--new-items" role="listbox">
2
- <%= render Katalyst::Content::Editor::NewItemComponent.with_collection(items) %>
1
+ <div class="content--editor--new-items"
2
+ data-controller="<%= NEW_ITEMS_CONTROLLER %>"
3
+ data-action="turbo:before-morph-element-><%= NEW_ITEMS_CONTROLLER %>#morph">
4
+ <%= tag.button(aria: { controls: "#{NEW_ITEMS_CONTROLLER}-dialog" },
5
+ class: "content--editor--add-button",
6
+ data: { action: "#{NEW_ITEMS_CONTROLLER}#open" }) do %>
7
+ Add content
8
+ <% end %>
9
+ <%= tag.div(class: "content--editor--inline-add",
10
+ data: { "#{NEW_ITEMS_CONTROLLER}_target" => "inline" },
11
+ hidden: "") do %>
12
+ <%= tag.button(aria: { controls: "#{NEW_ITEMS_CONTROLLER}-dialog", label: "Add content here" },
13
+ data: { action: "#{NEW_ITEMS_CONTROLLER}#open" }) %>
14
+ <% end %>
15
+ <%= tag.dialog(id: "#{NEW_ITEMS_CONTROLLER}-dialog", data: { action: "click->#{NEW_ITEMS_CONTROLLER}#close"}) do %>
16
+ <%= tag.div(role: "list", data: { action: "click->#{NEW_ITEMS_CONTROLLER}#noop:stop" }) do %>
17
+ <%= render Katalyst::Content::Editor::NewItemComponent.with_collection(items) %>
18
+ <% end %>
19
+ <% end %>
3
20
  </div>
@@ -7,8 +7,6 @@ module Katalyst
7
7
  ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
8
8
  dragstart->#{LIST_CONTROLLER}#dragstart
9
9
  dragover->#{LIST_CONTROLLER}#dragover
10
- dragenter->#{LIST_CONTROLLER}#dragenter
11
- dragleave->#{LIST_CONTROLLER}#dragleave
12
10
  drop->#{LIST_CONTROLLER}#drop
13
11
  dragend->#{LIST_CONTROLLER}#dragend
14
12
  keyup.esc@document->#{LIST_CONTROLLER}#dragend
@@ -6,4 +6,6 @@
6
6
  <%= render Katalyst::Content::Editor::TableComponent.new(container:) do |list| %>
7
7
  <%= container.draft_items&.each { |item| list.with_item(item) } %>
8
8
  <% end %>
9
+
10
+ <%= render Katalyst::Content::Editor::NewItemsComponent.new(container:) %>
9
11
  <% end %>
@@ -24,8 +24,10 @@ module Katalyst
24
24
  Editor::StatusBarComponent.new(container:)
25
25
  end
26
26
 
27
+ # @deprecated this component is now part of the editor
27
28
  def new_items
28
- Editor::NewItemsComponent.new(container:)
29
+ # no-op, no longer required
30
+ Class.new { define_method(:render_in) { |_| nil } }.new
29
31
  end
30
32
 
31
33
  def item_editor(item:)
@@ -3,6 +3,7 @@
3
3
  module Katalyst
4
4
  module Content
5
5
  class ApplicationController < ActionController::Base
6
+ protect_from_forgery with: :exception
6
7
  end
7
8
  end
8
9
  end
@@ -48,8 +48,8 @@ module Katalyst
48
48
  private
49
49
 
50
50
  def item_params_type
51
- type = params.require(:item).fetch(:type, "")
52
- if Katalyst::Content.config.items.include?(type)
51
+ requested_type = params.require(:item).fetch(:type, "")
52
+ if (type = Katalyst::Content.config.items.find { |t| t == requested_type })
53
53
  type.safe_constantize
54
54
  else
55
55
  Item
@@ -57,7 +57,7 @@ module Katalyst
57
57
  end
58
58
 
59
59
  def item_params
60
- params.require(:item).permit(item_params_type.permitted_params)
60
+ params.expect(item: item_params_type.permitted_params)
61
61
  end
62
62
 
63
63
  def set_container
@@ -1,7 +1,7 @@
1
1
  import ContainerController from "./editor/container_controller";
2
2
  import ItemController from "./editor/item_controller";
3
3
  import ListController from "./editor/list_controller";
4
- import NewItemController from "./editor/new_item_controller";
4
+ import NewItemsController from "./editor/new_items_controller";
5
5
  import StatusBarController from "./editor/status_bar_controller";
6
6
  import TableController from "./editor/table_controller";
7
7
  import TrixController from "./editor/trix_controller";
@@ -20,8 +20,8 @@ const Definitions = [
20
20
  controllerConstructor: ListController,
21
21
  },
22
22
  {
23
- identifier: "content--editor--new-item",
24
- controllerConstructor: NewItemController,
23
+ identifier: "content--editor--new-items",
24
+ controllerConstructor: NewItemsController,
25
25
  },
26
26
  {
27
27
  identifier: "content--editor--status-bar",
@@ -1,10 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  export default class ListController extends Controller {
4
- connect() {
5
- this.enterCount = 0;
6
- }
7
-
8
4
  /**
9
5
  * When the user starts a drag within the list, set the item's dataTransfer
10
6
  * properties to indicate that it's being dragged and update its style.
@@ -28,11 +24,6 @@ export default class ListController extends Controller {
28
24
  * When the user drags an item over another item in the last, swap the
29
25
  * dragging item with the item under the cursor.
30
26
  *
31
- * As a special case, if the item is dragged over placeholder space at the end
32
- * of the list, move the item to the bottom of the list instead. This allows
33
- * users to hit the list element more easily when adding new items to an empty
34
- * list.
35
- *
36
27
  * @param event {DragEvent}
37
28
  */
38
29
  dragover(event) {
@@ -46,54 +37,7 @@ export default class ListController extends Controller {
46
37
  }
47
38
 
48
39
  /**
49
- * When the user drags an item into the list, create a placeholder item to
50
- * represent the new item. Note that we can't access the drag data
51
- * until drop, so we assume that this is our template item for now.
52
- *
53
- * Users can cancel the drag by dragging the item out of the list or by
54
- * pressing escape. Both are handled by `cancelDrag`.
55
- *
56
- * @param event {DragEvent}
57
- */
58
- dragenter(event) {
59
- event.preventDefault();
60
-
61
- // Safari doesn't support relatedTarget, so we count enter/leave pairs
62
- this.enterCount++;
63
-
64
- if (copyAllowed(event) && !this.dragItem) {
65
- const item = document.createElement("li");
66
- item.dataset.dragging = "";
67
- item.dataset.newItem = "";
68
- this.element.appendChild(item);
69
- }
70
- }
71
-
72
- /**
73
- * When the user drags the item out of the list, remove the placeholder.
74
- * This allows users to cancel the drag by dragging the item out of the list.
75
- *
76
- * @param event {DragEvent}
77
- */
78
- dragleave(event) {
79
- // Safari doesn't support relatedTarget, so we count enter/leave pairs
80
- // https://bugs.webkit.org/show_bug.cgi?id=66547
81
- this.enterCount--;
82
-
83
- if (
84
- this.enterCount <= 0 &&
85
- this.dragItem?.dataset.hasOwnProperty("newItem")
86
- ) {
87
- this.dragItem.remove();
88
- this.reset();
89
- }
90
- }
91
-
92
- /**
93
- * When the user drops an item into the list, end the drag and reindex the list.
94
- *
95
- * If the item is a new item, we replace the placeholder with the template
96
- * item data from the dataTransfer API.
40
+ * When the user drops an item, end the drag and reindex the list.
97
41
  *
98
42
  * @param event {DragEvent}
99
43
  */
@@ -106,32 +50,17 @@ export default class ListController extends Controller {
106
50
  delete item.dataset.dragging;
107
51
  swap(dropTarget(event.target), item);
108
52
 
109
- if (item.dataset.hasOwnProperty("newItem")) {
110
- const placeholder = item;
111
- const template = document.createElement("template");
112
- template.innerHTML = event.dataTransfer.getData("text/html");
113
- item = template.content.querySelector("li");
114
-
115
- this.element.replaceChild(item, placeholder);
116
- requestAnimationFrame(() =>
117
- item.querySelector("[role='button'][value='edit']").click(),
118
- );
119
- }
120
-
121
53
  this.dispatch("drop", { target: item, bubbles: true, prefix: "content" });
122
54
  }
123
55
 
124
56
  /**
125
- * End an in-progress drag. If the item is a new item, remove it, otherwise
126
- * reset the item's style and restore its original position in the list.
57
+ * End an in-progress drag by resetting the item's style and restoring its
58
+ * original position in the list.
127
59
  */
128
60
  dragend() {
129
61
  const item = this.dragItem;
130
62
 
131
- if (!item) {
132
- } else if (item.dataset.hasOwnProperty("newItem")) {
133
- item.remove();
134
- } else {
63
+ if (item) {
135
64
  delete item.dataset.dragging;
136
65
  this.reset();
137
66
  }
@@ -155,7 +84,7 @@ export default class ListController extends Controller {
155
84
  }
156
85
 
157
86
  /**
158
- * Swaps two list items. If target is a list, the item is appended.
87
+ * Swaps two list items.
159
88
  *
160
89
  * @param target the target element to swap with
161
90
  * @param item the item the user is dragging
@@ -164,39 +93,17 @@ function swap(target, item) {
164
93
  if (!target) return;
165
94
  if (target === item) return;
166
95
 
167
- if (target.nodeName === "LI") {
168
- const positionComparison = target.compareDocumentPosition(item);
169
- if (positionComparison & Node.DOCUMENT_POSITION_FOLLOWING) {
170
- target.insertAdjacentElement("beforebegin", item);
171
- } else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {
172
- target.insertAdjacentElement("afterend", item);
173
- }
174
- }
175
-
176
- if (target.nodeName === "OL") {
177
- target.appendChild(item);
96
+ const positionComparison = target.compareDocumentPosition(item);
97
+ if (positionComparison & Node.DOCUMENT_POSITION_FOLLOWING) {
98
+ target.insertAdjacentElement("beforebegin", item);
99
+ } else if (positionComparison & Node.DOCUMENT_POSITION_PRECEDING) {
100
+ target.insertAdjacentElement("afterend", item);
178
101
  }
179
102
  }
180
103
 
181
- /**
182
- * Returns true if the event supports copy or copy move.
183
- *
184
- * Chrome and Firefox use copy, but Safari only supports copyMove.
185
- */
186
- function copyAllowed(event) {
187
- return (
188
- event.dataTransfer.effectAllowed === "copy" ||
189
- event.dataTransfer.effectAllowed === "copyMove"
190
- );
191
- }
192
-
193
104
  /**
194
105
  * Given an event target, return the closest drop target, if any.
195
106
  */
196
107
  function dropTarget(e) {
197
- return (
198
- e &&
199
- (e.closest("[data-controller='content--editor--list'] > *") ||
200
- e.closest("[data-controller='content--editor--list']"))
201
- );
108
+ return e && e.closest("[data-controller='content--editor--list'] > *");
202
109
  }
@@ -0,0 +1,143 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ const EDGE_AREA = 24;
4
+
5
+ export default class NewItemsController extends Controller {
6
+ static targets = ["inline"];
7
+
8
+ connect() {
9
+ this.form.addEventListener("mousemove", this.move);
10
+ }
11
+
12
+ disconnect() {
13
+ this.form?.removeEventListener("mousemove", this.move);
14
+ delete this.currentItem;
15
+ }
16
+
17
+ open(e) {
18
+ e.preventDefault();
19
+ this.dialog.showModal();
20
+ }
21
+
22
+ close(e) {
23
+ e.preventDefault();
24
+ this.dialog.close();
25
+ }
26
+
27
+ noop(e) {}
28
+
29
+ /**
30
+ * Add the selected item to the DOM at the current position or the end of the list.
31
+ */
32
+ add(e) {
33
+ e.preventDefault();
34
+
35
+ const template = e.target.querySelector("template");
36
+ const item = template.content.querySelector("li").cloneNode(true);
37
+ const target = this.currentItem;
38
+
39
+ if (target) {
40
+ target.insertAdjacentElement("beforebegin", item);
41
+ } else {
42
+ this.list.insertAdjacentElement("beforeend", item);
43
+ }
44
+
45
+ this.toggleInline(false);
46
+ this.dialog.close();
47
+
48
+ requestAnimationFrame(() => {
49
+ item.querySelector(`[value="edit"]`).click();
50
+ });
51
+ }
52
+
53
+ morph(e) {
54
+ e.preventDefault();
55
+ this.dialog.close();
56
+ }
57
+
58
+ move = (e) => {
59
+ if (this.isOverInlineTarget(e)) return;
60
+ if (this.dialog.open) return;
61
+
62
+ const target = this.getCurrentItem(e);
63
+
64
+ // return if we're already showing this item
65
+ if (this.currentItem === target) return;
66
+
67
+ // hide the button if it's already visible
68
+ if (this.currentItem) this.toggleInline(false);
69
+
70
+ this.currentItem = target;
71
+
72
+ // clear any previously set timer
73
+ if (this.timer) clearTimeout(this.timer);
74
+
75
+ // show the button after a debounce pause
76
+ this.timer = setTimeout(() => {
77
+ delete this.timer;
78
+ this.toggleInline();
79
+ }, 100);
80
+ };
81
+
82
+ toggleInline(show = !!this.currentItem) {
83
+ if (show) {
84
+ this.inlineTarget.style.top = `${this.currentItem.offsetTop}px`;
85
+ this.inlineTarget.toggleAttribute("hidden", false);
86
+ } else {
87
+ this.inlineTarget.toggleAttribute("hidden", true);
88
+ }
89
+ }
90
+
91
+ get dialog() {
92
+ return this.element.querySelector("dialog");
93
+ }
94
+
95
+ /**
96
+ * @returns {HTMLFormElement}
97
+ */
98
+ get form() {
99
+ return this.element.closest("form");
100
+ }
101
+
102
+ /**
103
+ * @returns {HTMLUListElement,null}
104
+ */
105
+ get list() {
106
+ return this.form.querySelector(`[data-controller="content--editor--list"]`);
107
+ }
108
+
109
+ /**
110
+ * @param {MouseEvent} e
111
+ * @returns {HTMLLIElement,null}
112
+ */
113
+ getCurrentItem(e) {
114
+ const item = document.elementFromPoint(e.clientX, e.clientY).closest("li");
115
+ if (!item) return null;
116
+
117
+ const bounds = item.getBoundingClientRect();
118
+
119
+ // check X for center(ish) mouse position
120
+ if (e.clientX < bounds.left + bounds.width / 2 - 2 * EDGE_AREA) return null;
121
+ if (e.clientX > bounds.left + bounds.width / 2 + 2 * EDGE_AREA) return null;
122
+
123
+ // check Y for hits on this item or it's next sibling
124
+ if (e.clientY - bounds.y <= EDGE_AREA) {
125
+ return item;
126
+ } else if (bounds.y + bounds.height - e.clientY <= EDGE_AREA) {
127
+ return item.nextElementSibling;
128
+ } else {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * @param {MouseEvent} e
135
+ * @returns {Boolean} true when the target of the event is the floating button
136
+ */
137
+ isOverInlineTarget(e) {
138
+ return (
139
+ this.inlineTarget ===
140
+ document.elementFromPoint(e.clientX, e.clientY).closest("div")
141
+ );
142
+ }
143
+ }
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage_validations"
4
-
5
3
  module Katalyst
6
4
  module Content
7
5
  class Figure < Item
@@ -78,7 +78,8 @@ module Katalyst
78
78
  def header_row_caption?
79
79
  !at_css("caption") &&
80
80
  (tr = at_css("thead > tr:first-child"))&.elements&.count == 1 &&
81
- (tr.elements.first.attributes["colspan"]&.value&.to_i&.> 1)
81
+ (colspan = tr.elements.first.attributes["colspan"]&.value) &&
82
+ colspan.to_i > 1
82
83
  end
83
84
 
84
85
  def normalize_emphasis!
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_storage_validations"
3
4
  require "active_support"
4
5
 
5
6
  module Katalyst
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-content
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-09-17 00:00:00.000000000 Z
10
+ date: 2025-03-03 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -80,7 +79,6 @@ dependencies:
80
79
  - - ">="
81
80
  - !ruby/object:Gem::Version
82
81
  version: '0'
83
- description:
84
82
  email:
85
83
  - developers@katalyst.com.au
86
84
  executables: []
@@ -135,7 +133,7 @@ files:
135
133
  - app/javascript/content/editor/item.js
136
134
  - app/javascript/content/editor/item_controller.js
137
135
  - app/javascript/content/editor/list_controller.js
138
- - app/javascript/content/editor/new_item_controller.js
136
+ - app/javascript/content/editor/new_items_controller.js
139
137
  - app/javascript/content/editor/rules_engine.js
140
138
  - app/javascript/content/editor/status_bar_controller.js
141
139
  - app/javascript/content/editor/table_controller.js
@@ -198,7 +196,6 @@ licenses:
198
196
  - MIT
199
197
  metadata:
200
198
  rubygems_mfa_required: 'true'
201
- post_install_message:
202
199
  rdoc_options: []
203
200
  require_paths:
204
201
  - lib
@@ -213,8 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
210
  - !ruby/object:Gem::Version
214
211
  version: '0'
215
212
  requirements: []
216
- rubygems_version: 3.5.16
217
- signing_key:
213
+ rubygems_version: 3.6.2
218
214
  specification_version: 4
219
215
  summary: Rich content page builder and editor
220
216
  test_files: []
@@ -1,12 +0,0 @@
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
- }