katalyst-content 2.7.1 → 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
@@ -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
+ }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-content
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.1
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-02-26 00:00:00.000000000 Z
10
+ date: 2025-03-03 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -133,7 +133,7 @@ files:
133
133
  - app/javascript/content/editor/item.js
134
134
  - app/javascript/content/editor/item_controller.js
135
135
  - app/javascript/content/editor/list_controller.js
136
- - app/javascript/content/editor/new_item_controller.js
136
+ - app/javascript/content/editor/new_items_controller.js
137
137
  - app/javascript/content/editor/rules_engine.js
138
138
  - app/javascript/content/editor/status_bar_controller.js
139
139
  - app/javascript/content/editor/table_controller.js
@@ -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
- }