katalyst-content 0.2.0 → 0.2.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: 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