katalyst-content 0.2.0 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4834b72d776bd56e8a930a70c5f60880ffee763fda2aa4cb071c4584091ac8fe
4
- data.tar.gz: 5987c1c93e5731c9875ba820560e2167fdea33b2e6e3e26a1edb1dfb4464b2b4
3
+ metadata.gz: 5d759a4e97d9609138eba1c1d03ad9a081cc1badb65be59b5b200a23665909e7
4
+ data.tar.gz: 848034cbe45b3fb9c0550bd452c5424cccdf5c4a7e4e9b819abaef0bed9b24a3
5
5
  SHA512:
6
- metadata.gz: bf0e0825807b3736ccdfcfd49b7e1c09a4a9b41878fbb5d6d3c582dd9ef525069f131b960e927e0358647526c94f010b99836676a02cb455139eaba1e30b30d6
7
- data.tar.gz: ef847109661189423b6ce3de4034531e1203c17164dd92f7622ad97dd06d39a7e9c854e8595cbde9abe1dd609331ecb4d284a15f2da5b318112b3a075f87651a
6
+ metadata.gz: 64f111753041dd647e73d863e011b23c3507cb930dcac40dd55f991d422802896cc6d515e998adf31be3a07962a38ac8b22e16490b3a56fcb3638441bd8d5d26
7
+ data.tar.gz: 24e97c6e52f38944a1947b50cb09347c3e5ebd457bcdb7bff0aff70aeac8412ed0a1ba8cfc85606d26886f30499c3981c4d4de414d2651c38d08f4104b362e79
@@ -26,6 +26,36 @@ export default class ContainerController extends Controller {
26
26
  this.container.reset();
27
27
  }
28
28
 
29
+ drop(event) {
30
+ this.container.reindex(); // set indexes before calculating previous
31
+
32
+ const item = getEventItem(event);
33
+ const previous = item.previousItem;
34
+
35
+ let delta = 0;
36
+ if (previous === undefined) {
37
+ // if previous does not exist, set depth to 0
38
+ delta = -item.depth;
39
+ } else if (
40
+ previous.isLayout &&
41
+ item.nextItem &&
42
+ item.nextItem.depth > previous.depth
43
+ ) {
44
+ // if previous is a layout and next is a child of previous, make item a child of previous
45
+ delta = previous.depth - item.depth + 1;
46
+ } else {
47
+ // otherwise, make item a sibling of previous
48
+ delta = previous.depth - item.depth;
49
+ }
50
+
51
+ item.traverse((child) => {
52
+ child.depth += delta;
53
+ });
54
+
55
+ this.#update();
56
+ event.preventDefault();
57
+ }
58
+
29
59
  remove(event) {
30
60
  const item = getEventItem(event);
31
61
 
@@ -38,7 +68,9 @@ export default class ContainerController extends Controller {
38
68
  nest(event) {
39
69
  const item = getEventItem(event);
40
70
 
41
- item.nest();
71
+ item.traverse((child) => {
72
+ child.depth += 1;
73
+ });
42
74
 
43
75
  this.#update();
44
76
  event.preventDefault();
@@ -47,7 +79,9 @@ export default class ContainerController extends Controller {
47
79
  deNest(event) {
48
80
  const item = getEventItem(event);
49
81
 
50
- item.deNest();
82
+ item.traverse((child) => {
83
+ child.depth -= 1;
84
+ });
51
85
 
52
86
  this.#update();
53
87
  event.preventDefault();
@@ -46,7 +46,7 @@ export default class ListController extends Controller {
46
46
  }
47
47
 
48
48
  drop(event) {
49
- const item = this.dragItem();
49
+ let item = this.dragItem();
50
50
 
51
51
  if (!item) return;
52
52
 
@@ -55,17 +55,18 @@ export default class ListController extends Controller {
55
55
  swap(this.dropTarget(event.target), item);
56
56
 
57
57
  if (item.dataset.hasOwnProperty("newItem")) {
58
+ const placeholder = item;
58
59
  const template = document.createElement("template");
59
60
  template.innerHTML = event.dataTransfer.getData("text/html");
60
- const newItem = template.content.querySelector("li");
61
+ item = template.content.querySelector("li");
61
62
 
62
- this.element.replaceChild(newItem, item);
63
+ this.element.replaceChild(item, placeholder);
63
64
  setTimeout(() =>
64
- newItem.querySelector("[role='button'][value='edit']").click()
65
+ item.querySelector("[role='button'][value='edit']").click()
65
66
  );
66
67
  }
67
68
 
68
- this.reindex();
69
+ this.dispatch("drop", { target: item, bubbles: true, prefix: "content" });
69
70
  }
70
71
 
71
72
  dragend() {
@@ -25,7 +25,7 @@ delete Trix.config.blockAttributes.heading1;
25
25
  * input does not permit. Uses a permissive regex pattern which is not suitable
26
26
  * for untrusted use cases.
27
27
  */
28
- const LINK_PATTERN = "(https?|/|#).*?";
28
+ const LINK_PATTERN = "(https?|mailto:|tel:|/|#).*?";
29
29
 
30
30
  /**
31
31
  * Customize default toolbar:
@@ -76,3 +76,13 @@ Trix.config.toolbar.getDefaultHTML = () => {
76
76
  </div>
77
77
  `;
78
78
  };
79
+
80
+ /**
81
+ * If the <trix-editor> element is in the HTML when Trix loads, then Trix will have already injected the toolbar content
82
+ * before our code gets a chance to run. Fix that now.
83
+ *
84
+ * Note: in Trix 2 this is likely to no longer be necessary.
85
+ */
86
+ document.querySelectorAll("trix-toolbar").forEach((e) => {
87
+ e.innerHTML = Trix.config.toolbar.getDefaultHTML();
88
+ });
@@ -131,7 +131,7 @@ export default class Item {
131
131
 
132
132
  callback(this);
133
133
  this.#traverseCollapsed(callback);
134
- expanded.forEach((item) => this.#traverseCollapsed(item));
134
+ expanded.forEach((item) => item.#traverseCollapsed(callback));
135
135
  }
136
136
 
137
137
  /**
@@ -148,16 +148,6 @@ export default class Item {
148
148
  });
149
149
  }
150
150
 
151
- /**
152
- * Increase the depth of this item and its descendants.
153
- * If this causes it to become a child of a collapsed item, then collapse this item.
154
- */
155
- nest() {
156
- this.traverse((child) => {
157
- child.depth += 1;
158
- });
159
- }
160
-
161
151
  /**
162
152
  * Move the given item into this element's hidden children list.
163
153
  * Assumes the list already exists.
@@ -168,15 +158,6 @@ export default class Item {
168
158
  this.#childrenListElement.appendChild(item.node);
169
159
  }
170
160
 
171
- /**
172
- * Decrease the depth of this item (and its descendants).
173
- */
174
- deNest() {
175
- this.traverse((child) => {
176
- child.depth -= 1;
177
- });
178
- }
179
-
180
161
  /**
181
162
  * Collapses visible (logical) children into this element's hidden children
182
163
  * list, creating it if it doesn't already exist.
@@ -59,7 +59,7 @@ export default class RulesEngine {
59
59
  * First item can't have a parent, so its depth should always be 0
60
60
  */
61
61
  firstItemDepthZero(item) {
62
- if (item.index === 0 && !item.depth === 0) {
62
+ if (item.index === 0 && item.depth !== 0) {
63
63
  this.debug(`enforce depth on item ${item.index}: ${item.depth} => 0`);
64
64
 
65
65
  item.depth = 0;
@@ -10,6 +10,7 @@
10
10
  flex-direction: column;
11
11
  justify-content: center;
12
12
  align-items: center;
13
+ text-align: center;
13
14
  transform: translate3d(0, 0, 0);
14
15
  cursor: grab;
15
16
  background: white;
@@ -13,6 +13,10 @@ module Katalyst
13
13
  render locals: { item: @container.items.build(item_params) }
14
14
  end
15
15
 
16
+ def edit
17
+ render locals: { item: @item }
18
+ end
19
+
16
20
  def create
17
21
  @item = item = @container.items.build(item_params)
18
22
  if item.save
@@ -22,10 +26,6 @@ module Katalyst
22
26
  end
23
27
  end
24
28
 
25
- def edit
26
- render locals: { item: @item }
27
- end
28
-
29
29
  def update
30
30
  @item.attributes = item_params
31
31
 
@@ -6,6 +6,7 @@ module Katalyst
6
6
  class Container < Base
7
7
  ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
8
8
  submit->#{CONTAINER_CONTROLLER}#reindex
9
+ content:drop->#{CONTAINER_CONTROLLER}#drop
9
10
  content:reindex->#{CONTAINER_CONTROLLER}#reindex
10
11
  content:reset->#{CONTAINER_CONTROLLER}#reset
11
12
  ACTIONS
@@ -13,12 +14,17 @@ module Katalyst
13
14
  def build(options)
14
15
  form_with(model: container, **default_options(id: container_form_id, **options)) do |form|
15
16
  concat hidden_input
17
+ concat errors
16
18
  concat(capture { yield form })
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
24
+ def errors
25
+ Editor::Errors.new(self, container).build
26
+ end
27
+
22
28
  # Hidden input ensures that if the container is empty then the controller
23
29
  # receives an empty array.
24
30
  def hidden_input
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Content
5
+ module Editor
6
+ class Errors < Base
7
+ def build(**options)
8
+ turbo_frame_tag dom_id(container, :errors) do
9
+ next unless container.errors.any?
10
+
11
+ tag.div(class: "content-errors", **options) do
12
+ tag.h2("Errors in content") +
13
+ tag.ul(class: "errors") do
14
+ container.errors.each do |error|
15
+ concat(tag.li(error.full_message))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -25,6 +25,12 @@ module Katalyst
25
25
  Editor::List.new(self, container).items(item)
26
26
  end
27
27
 
28
+ # Generate a turbo stream fragment that will show structural errors to the user.
29
+ def content_editor_errors(container:, **options)
30
+ turbo_stream.replace(dom_id(container, :errors),
31
+ Editor::Errors.new(self, container).build(**options))
32
+ end
33
+
28
34
  # Gene
29
35
  def content_editor_new_item(item:, container: item.container, **options, &block)
30
36
  Editor::NewItem.new(self, container).build(item, **options, &block)
@@ -95,6 +95,11 @@ module Katalyst
95
95
  self
96
96
  end
97
97
 
98
+ # Required for testing items validation
99
+ def items_attributes
100
+ draft_version&.nodes&.as_json
101
+ end
102
+
98
103
  # Updates the current draft version with new structure. Attributes should be structural information about the
99
104
  # items, e.g. `{index => {id:, depth:}` or `[{id:, depth:}]`.
100
105
  #
@@ -13,6 +13,12 @@ module Katalyst
13
13
  # rubocop:enable Rails/ReflectionClassName
14
14
 
15
15
  attribute :nodes, Katalyst::Content::Types::NodesType.new, default: -> { [] }
16
+
17
+ validate :ensure_items_exists
18
+ end
19
+
20
+ def ensure_items_exists
21
+ parent.errors.add(:items, :missing_item) unless items.all?(&:present?)
16
22
  end
17
23
 
18
24
  def items
@@ -22,10 +28,10 @@ module Katalyst
22
28
 
23
29
  items = parent.items.where(id: nodes.map(&:id)).index_by(&:id)
24
30
  nodes.map do |node|
25
- item = items[node.id]
26
- item.index = node.index
27
- item.depth = node.depth
28
- item
31
+ items[node.id]&.tap do |item|
32
+ item.index = node.index
33
+ item.depth = node.depth
34
+ end
29
35
  end
30
36
  end
31
37
 
@@ -20,7 +20,7 @@ module Katalyst
20
20
  # if image has changed, duplicate the change, otherwise attach the existing blob
21
21
  if source.attachment_changes["image"]
22
22
  self.image = source.attachment_changes["image"].attachable
23
- elsif source.image.attached?
23
+ elsif source.image.attached? && !source.image.marked_for_destruction?
24
24
  image.attach(source.image.blob)
25
25
  end
26
26
  end
@@ -11,7 +11,7 @@ module Katalyst
11
11
  belongs_to :container, polymorphic: true
12
12
 
13
13
  validates :heading, presence: true
14
- validates :background, presence: true, inclusion: { in: config.backgrounds }
14
+ validates :background, presence: true, inclusion: { in: config.backgrounds }, if: :validate_background?
15
15
 
16
16
  after_initialize :initialize_tree
17
17
 
@@ -43,6 +43,10 @@ module Katalyst
43
43
  self.parent ||= nil
44
44
  self.children ||= []
45
45
  end
46
+
47
+ def validate_background?
48
+ true
49
+ end
46
50
  end
47
51
  end
48
52
  end
@@ -8,7 +8,7 @@
8
8
  <%= render_content_items items %>
9
9
  </div>
10
10
  <div class="column">
11
- <%= render_content_items [last] %>
11
+ <%= render_content_items [last] if last %>
12
12
  </div>
13
13
  </div>
14
14
  <% end %>
@@ -2,16 +2,10 @@
2
2
  <%= render "hidden_fields", form: form %>
3
3
  <%= render "form_errors", form: form %>
4
4
 
5
- <%= content_editor_image_field item: figure, method: :image do |builder| %>
6
- <div class="field">
7
- <%= form.label :image %>
8
- <div>
9
- <%= builder.preview class: "image-wrapper" %>
10
- </div>
11
- <%= form.file_field :image, **builder.file_input_options %>
12
- <hint><%= builder.hint_text %></hint>
13
- </div>
14
- <% end %>
5
+ <div class="field">
6
+ <%= form.label :image %>
7
+ <%= form.file_field :image %>
8
+ </div>
15
9
 
16
10
  <div class="field">
17
11
  <%= form.label :heading %>
@@ -4,6 +4,8 @@ en:
4
4
  katalyst/content/figure:
5
5
  heading: "Alternate text"
6
6
  errors:
7
+ messages:
8
+ missing_item: Content items are missing or invalid
7
9
  models:
8
10
  katalyst/content/item:
9
11
  content_type_invalid: file type not supported
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Katalyst
4
4
  module Content
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-content
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.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-10-11 00:00:00.000000000 Z
11
+ date: 2022-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_storage_validations
@@ -35,7 +35,6 @@ files:
35
35
  - README.md
36
36
  - app/assets/config/katalyst-content.js
37
37
  - app/assets/javascripts/controllers/content/editor/container_controller.js
38
- - app/assets/javascripts/controllers/content/editor/image_field_controller.js
39
38
  - app/assets/javascripts/controllers/content/editor/item_controller.js
40
39
  - app/assets/javascripts/controllers/content/editor/list_controller.js
41
40
  - app/assets/javascripts/controllers/content/editor/new_item_controller.js
@@ -58,7 +57,7 @@ files:
58
57
  - app/controllers/katalyst/content/items_controller.rb
59
58
  - app/helpers/katalyst/content/editor/base.rb
60
59
  - app/helpers/katalyst/content/editor/container.rb
61
- - app/helpers/katalyst/content/editor/image_field.rb
60
+ - app/helpers/katalyst/content/editor/errors.rb
62
61
  - app/helpers/katalyst/content/editor/item.rb
63
62
  - app/helpers/katalyst/content/editor/list.rb
64
63
  - app/helpers/katalyst/content/editor/new_item.rb
@@ -1,90 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus";
2
-
3
- export default class ImageFieldController extends Controller {
4
- static targets = ["preview"];
5
- static values = {
6
- mimeTypes: Array,
7
- };
8
-
9
- #counter = 0;
10
-
11
- onUpload(event) {
12
- this.previewTarget.classList.remove("hidden");
13
-
14
- const reader = new FileReader();
15
-
16
- reader.onload = (e) => {
17
- this.imageTag.src = e.target.result;
18
- };
19
-
20
- reader.readAsDataURL(event.currentTarget.files[0]);
21
- }
22
-
23
- drop(event) {
24
- event.preventDefault();
25
-
26
- const file = fileForEvent(event, this.mimeTypesValue);
27
- if (file) {
28
- const dT = new DataTransfer();
29
- dT.items.add(file);
30
- this.fileInput.files = dT.files;
31
- this.fileInput.dispatchEvent(new Event("change"));
32
- }
33
-
34
- this.#counter = 0;
35
- this.element.classList.remove("droppable");
36
- }
37
-
38
- dragover(event) {
39
- event.preventDefault();
40
- }
41
-
42
- dragenter(event) {
43
- event.preventDefault();
44
-
45
- if (this.#counter === 0) {
46
- this.element.classList.add("droppable");
47
- }
48
- this.#counter++;
49
- }
50
-
51
- dragleave(event) {
52
- event.preventDefault();
53
-
54
- this.#counter--;
55
- if (this.#counter === 0) {
56
- this.element.classList.remove("droppable");
57
- }
58
- }
59
-
60
- get fileInput() {
61
- return this.element.querySelector("input[type='file']");
62
- }
63
-
64
- get imageTag() {
65
- return this.previewTarget.querySelector("img");
66
- }
67
- }
68
-
69
- /**
70
- * Given a drop event, find the first acceptable file.
71
- * @param event {DropEvent}
72
- * @param mimeTypes {String[]}
73
- * @returns {File}
74
- */
75
- function fileForEvent(event, mimeTypes) {
76
- const accept = (file) => mimeTypes.indexOf(file.type) > -1;
77
-
78
- let file;
79
-
80
- if (event.dataTransfer.items) {
81
- const item = [...event.dataTransfer.items].find(accept);
82
- if (item) {
83
- file = item.getAsFile();
84
- }
85
- } else {
86
- file = [...event.dataTransfer.files].find(accept);
87
- }
88
-
89
- return file;
90
- }
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Katalyst
4
- module Content
5
- module Editor
6
- class ImageField < Base
7
- IMAGE_FIELD_CONTROLLER = "content--editor--image-field"
8
-
9
- ACTIONS = <<~ACTIONS.gsub(/\s+/, " ").freeze
10
- dragover->#{IMAGE_FIELD_CONTROLLER}#dragover
11
- dragenter->#{IMAGE_FIELD_CONTROLLER}#dragenter
12
- dragleave->#{IMAGE_FIELD_CONTROLLER}#dragleave
13
- drop->#{IMAGE_FIELD_CONTROLLER}#drop
14
- ACTIONS
15
-
16
- attr_accessor :item, :method
17
-
18
- def build(item, method, **options, &block)
19
- self.item = item
20
- self.method = method
21
-
22
- tag.div **default_options(**options) do
23
- concat(capture { yield self }) if block
24
- end
25
- end
26
-
27
- def preview(**options)
28
- add_option(options, :data, "#{IMAGE_FIELD_CONTROLLER}_target", "preview")
29
- add_option(options, :class, "hidden") unless preview?
30
-
31
- tag.div **options do
32
- image_tag preview_url, class: "image-thumbnail"
33
- end
34
- end
35
-
36
- def file_input_options(options = {})
37
- add_option(options, :accept, config.image_mime_types.join(","))
38
- add_option(options, :data, :action, "change->#{IMAGE_FIELD_CONTROLLER}#onUpload")
39
-
40
- options
41
- end
42
-
43
- def hint_text
44
- t("views.katalyst.content.item.size_hint", max_size: number_to_human_size(config.max_image_size.megabytes))
45
- end
46
-
47
- def preview?
48
- value&.attached? && value&.persisted?
49
- end
50
-
51
- def preview_url
52
- preview? ? main_app.url_for(value) : ""
53
- end
54
-
55
- def value
56
- item.send(method)
57
- end
58
-
59
- private
60
-
61
- def default_options(**options)
62
- add_option(options, :data, :controller, IMAGE_FIELD_CONTROLLER)
63
- add_option(options, :data, :action, ACTIONS)
64
- add_option(options, :data, :"#{IMAGE_FIELD_CONTROLLER}_mime_types_value",
65
- config.image_mime_types.to_json)
66
-
67
- options
68
- end
69
- end
70
- end
71
- end
72
- end