lexxy 0.1.3.beta → 0.1.5.beta

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: '00901cdcdde62136776fa01e90c26688136085b1612c38f42aede8e78b5750f0'
4
- data.tar.gz: ef892b4377ffd73531dde83c80f314f806e746fae8b9eb71cf6c5dfc34aa1c2c
3
+ metadata.gz: d97469124d8bd00ee856e66f48d8d8e7899188baf9d25b1692e72a1387bb5434
4
+ data.tar.gz: 0abfd04d0c32c8d4a6ee07d86deef71529370e9ab19755f64745d239e7d89002
5
5
  SHA512:
6
- metadata.gz: 3e2cde2dc5a349904bd4231b3b79c2c45b208df2558d0968c0311a5302273e3faaa357f315eac1763198025bac29950d4630b50204340171f467986a3d451222
7
- data.tar.gz: ef761ad7f6d56ec0fd44a33b173abd85a83483562fa1700997ff026f787839ac1f7a96948ec70f9f9ec437455670107e094987b3652d132bbdfc670e9afa7419
6
+ metadata.gz: 1331cfd0fd69bb5cbd89e23b398fea9bde0540fbf23e5cfe41a2bd3c40f451fa12d0d680606d50f1d881496bf8011a7e6008636cbffab91a161a6e59b86dcb81
7
+ data.tar.gz: 215fccc8ae0711d1a644c1f34b21f4c96ec642405ef3947e51f731b921d50c755783c9b1ffe0e903ce41c09c687db0aa83fb8209a51fafe73ba8bd7778428e0f
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A modern rich text editor for Rails.
4
4
 
5
- > [!IMPORTANT]
5
+ > [!IMPORTANT]
6
6
  > This is an early beta. It hasn't been battle-tested yet. Please try it out and report any issues you find.
7
7
 
8
8
  ## Features
@@ -23,7 +23,7 @@ A modern rich text editor for Rails.
23
23
  Add this line to your application's Gemfile:
24
24
 
25
25
  ```ruby
26
- gem 'lexxy'
26
+ gem 'lexxy', '~> 0.1.4.beta' # Need to specify the version since it's a pre-release
27
27
  ```
28
28
 
29
29
  And then execute:
@@ -32,24 +32,65 @@ And then execute:
32
32
  bundle install
33
33
  ```
34
34
 
35
- Then, you need to import the lexxy source in your app. If you are using [propshaft](https://github.com/rails/propshaft) and [import maps](https://github.com/rails/importmap-rails) you can do:
35
+ ### With import maps
36
+
37
+ If you are using [propshaft](https://github.com/rails/propshaft) and [import maps](https://github.com/rails/importmap-rails):
36
38
 
37
39
  ```ruby
38
40
  # importmap.rb
39
41
  pin "lexxy", to: "lexxy.js"
42
+ pin "@rails/activestorage", to: "activestorage.esm.js" # to support attachments
40
43
  ```
41
44
 
45
+ Then import it in your JavaScript entry point:
46
+
42
47
  ```javascript
43
- // application.js
48
+ // app/javascript/application.js
44
49
  import "lexxy"
45
50
  ```
46
51
 
52
+ ### With javascript bundlers
53
+
54
+ If you're using [jsbundling-rails](https://github.com/rails/jsbundling-rails), esbuild, webpack, or any other JavaScript bundler, you can install the NPM package:
55
+
56
+ ```bash
57
+ yarn add @37signals/lexxy
58
+ yarn add @rails/activestorage # to support attachments
59
+ ```
60
+
61
+ Then import it in your JavaScript entry point:
62
+
63
+ ```javascript
64
+ // app/javascript/application.js
65
+ import "@37signals/lexxy"
66
+ ```
67
+
68
+ ### Override Action Text defaults
69
+
70
+ By default, the gem overrides Action Text form helpers, so that if you use `form.rich_text_area`, it will render a Lexxy editor instead of the default Trix editor.
71
+
72
+ You can opt out of this behavior by disabling this option in `application.rb`:
73
+
74
+ ```ruby# config/application.rb
75
+ config.lexxy.override_action_text_defaults = false
76
+ ```
77
+
78
+ If you do this, you can invoke Lexxy explicitly using the same helpers with a `lexxy_preffix`: `lexxy_rich_textarea_tag` and `form.lexxy_rich_text_area`.
79
+
80
+ This path is meant to let you incrementally move to Lexxy, or to use it in specific places while keeping Trix in others.
81
+
82
+ ### CSS Setup
83
+
47
84
  For the CSS, you can include it with the standard Rails helper:
48
85
 
49
86
  ```erb
50
87
  <%= stylesheet_link_tag "lexxy" %>
51
88
  ```
52
89
 
90
+ Of course, you can copy the CSS to your project and adapt it to your needs.
91
+
92
+ #### Rendered Action Text content
93
+
53
94
  For applying the same styles to rendered Action Text content, you need to override the current default by adding this template `app/views/layouts/action_text/contents/_content.html.erb`:
54
95
 
55
96
  ```erb
@@ -58,10 +99,26 @@ For applying the same styles to rendered Action Text content, you need to overri
58
99
  </div>
59
100
  ```
60
101
 
61
- Of course, you can copy the CSS to your project and adapt it to your needs.
102
+ To apply syntax highlighting to rendered Action Text content, you need to call the `highlightAll` function from Lexxy. For example, create a Stimulus controller in `app/javascript/controllers/syntax_highlight_controller.js`:
62
103
 
63
- > [!NOTE]
64
- > We'll streamline the configuration process as we work towards a final release.
104
+ ```javascript
105
+ import { Controller } from "@hotwired/stimulus"
106
+ import { highlightAll } from "lexxy"
107
+
108
+ export default class extends Controller {
109
+ connect() {
110
+ highlightAll()
111
+ }
112
+ }
113
+ ```
114
+
115
+ Then update the Action Text Content template to include the `data-controller` attribute:
116
+
117
+ ```erb
118
+ <div data-controller="syntax-highlight" class="lexxy-content">
119
+ <%= yield -%>
120
+ </div>
121
+ ```
65
122
 
66
123
  ## Configuration
67
124
 
@@ -83,9 +140,9 @@ Under the hood, this will insert a `<lexxy-editor>` tag, that will be a first-cl
83
140
 
84
141
  The `<lexxy-editor>` element supports these options:
85
142
 
86
- - `placeholder` - Text displayed when the editor is empty.
87
- - `toolbar` - Pass `"false"` to disable the toolbar entirely, or pass an element ID to render the toolbar in an external element. By default, the toolbar is bootstrapped and displayed above the editor.
88
- - `attachments` - Pass `"false"` to disable attachments completely. By default, attachments are supported, including paste and Drag & Drop support.
143
+ - `placeholder`: Text displayed when the editor is empty.
144
+ - `toolbar`: Pass `"false"` to disable the toolbar entirely, or pass an element ID to render the toolbar in an external element. By default, the toolbar is bootstrapped and displayed above the editor.
145
+ - `attachments`: Pass `"false"` to disable attachments completely. By default, attachments are supported, including paste and Drag & Drop support.
89
146
 
90
147
  Lexxy uses the `ElementInternals` API to participate in HTML forms as any standard control. This means that you can use standard HTML attributes like `name`, `value`, `required`, `disabled`, etc.
91
148
 
@@ -103,7 +160,7 @@ Lexxy also lets you configure how to load the items: inline or remotely, and how
103
160
  The first thing to do is to add a `<lexxy-prompt>` element to the editor:
104
161
 
105
162
  ```erb
106
- <%= form_with model: @post do |form| %>
163
+ <%= form.rich_text_area :body do %>
107
164
  <lexxy-prompt trigger="@">
108
165
  </lexxy-prompt>
109
166
  <% end %>
@@ -135,12 +192,16 @@ Where:
135
192
 
136
193
  Imagine you want to implement a *mentions* feature, where users can type "@" and select a person to mention. You want to save mentions as action text attachments for further server-side processing when the form is submitted.
137
194
 
138
- First, you need to include the `ActionText::Attachable` concern in your model.
195
+ First, you need to include the `ActionText::Attachable` concern in your model, and you need to define the `#content_type` method to return a value like `application/vnd.actiontext.<prompt name>`, where `<prompt name>` is the value of the `name` attribute you will set in the `<lexxy-prompt>` element later. Let's use `mention` as the prompt name:
139
196
 
140
197
  ```ruby
141
198
  # app/models/person.rb
142
199
  class Person < ApplicationRecord
143
200
  include ActionText::Attachable
201
+
202
+ def content_type
203
+ "application/vnd.actiontext.mention"
204
+ end
144
205
  end
145
206
  ```
146
207
 
@@ -196,7 +257,7 @@ We could define the controller action to serve the prompt items like this:
196
257
  class PeopleController < ApplicationController
197
258
  def index
198
259
  @people = Person.all
199
-
260
+
200
261
  render layout: false
201
262
  end
202
263
  end
@@ -238,30 +299,31 @@ By default, the `SPACE` key will select the current item in the prompt. If you w
238
299
 
239
300
  #### `<lexxy-prompt>`
240
301
 
241
- - `trigger` - The character that activates the prompt (e.g., "@", "#", "/").
242
- - `src` - Path or URL to load items remotely.
243
- - `name` - Identifier for the prompt type (determines attachment content type, e.g., `name= "mention"` creates `application/vnd.actiontext.mention`). Mandatory unless using `insert-editable-text`.
244
- - `empty-results` - Message shown when no matches found. By default it is "Nothing found".
245
- - `remote-filtering` - Enable server-side filtering instead of loading all options at once.
246
- - `insert-editable-text` - Insert prompt item HTML directly as editable text instead of Action Text attachments.
247
- - `supports-space-in-searches` - Allow spaces in search queries (useful with remote filtering for full name searches).
302
+ - `trigger`: The character that activates the prompt (e.g., "@", "#", "/").
303
+ - `src`: Path or URL to load items remotely.
304
+ - `name`: Identifier for the prompt type (determines attachment content type, e.g., `name= "mention"` creates `application/vnd.actiontext.mention`). Mandatory unless using `insert-editable-text`.
305
+ - `empty-results`: Message shown when no matches found. By default it is "Nothing found".
306
+ - `remote-filtering`: Enable server-side filtering instead of loading all options at once.
307
+ - `insert-editable-text`: Insert prompt item HTML directly as editable text instead of Action Text attachments.
308
+ - `supports-space-in-searches`: Allow spaces in search queries (useful with remote filtering for full name searches).
248
309
 
249
310
  #### `<lexxy-prompt-item>`
250
311
 
251
- - `search` - The text to match against when filtering (can include multiple fields for better search).
252
- - `sgid` - The signed GlobalID for Action Text attachments (use `attachable_sgid` helper). Mandatory unless using `insert-editable-text`.
312
+ - `search`: The text to match against when filtering (can include multiple fields for better search).
313
+ - `sgid`: The signed GlobalID for Action Text attachments (use `attachable_sgid` helper). Mandatory unless using `insert-editable-text`.
253
314
 
254
315
  ## Roadmap
255
316
 
256
317
  This is an early beta. Here's what's coming next:
257
318
 
258
- - Configurable editors in Action Text - Choose your editor like you choose your database.
319
+ - Configurable editors in Action Text: Choose your editor like you choose your database.
259
320
  - More editing features:
260
321
  - Tables
261
322
  - Text highlighting
262
- - Image galleries - The only remaining feature for full Action Text compatibility
323
+ - Image galleries: The only remaining feature for full Action Text compatibility
263
324
  - Install task that generates the necessary JS and adds stylesheets.
264
- - Standalone JS package - to use in non-Rails environments.
325
+ - Configuration hooks.
326
+ - Standalone JS package: to use in non-Rails environments.
265
327
 
266
328
  ## Development
267
329
 
@@ -279,6 +341,12 @@ bin/rails server
279
341
 
280
342
  The sandbox app is available at http://localhost:3000. There is also a CRUD example at http://localhost:3000/posts.
281
343
 
344
+ ## Events
345
+
346
+ * `lexxy:initialize`: Fired whenever the `<lexxy-editor>` element is attached to the DOM and is ready for use.
347
+ * `lexxy:change`: Fired whenever the editor content changes.
348
+ * `lexxy:file-accept`: Fired whenever a file is dropped or inserted into the editor. You can access the `File` object through the `file` property. Call `preventDefault` on the event to cancel upload and prevent attaching the file.
349
+
282
350
  ## Contributing
283
351
 
284
352
  - Bug reports and pull requests are welcome on [GitHub Issues](https://github.com/basecamp/lexxy/issues). Help is especially welcome with [those tagged as "Help Wanted"](https://github.com/basecamp/lexxy/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
@@ -3746,6 +3746,21 @@ function getListType(node) {
3746
3746
  }
3747
3747
 
3748
3748
  class LexicalToolbarElement extends HTMLElement {
3749
+ constructor() {
3750
+ super();
3751
+ this.internals = this.attachInternals();
3752
+ this.internals.role = "toolbar";
3753
+ }
3754
+
3755
+ connectedCallback() {
3756
+ this.#refreshToolbarOverflow();
3757
+ window.addEventListener("resize", this.#refreshToolbarOverflow);
3758
+ }
3759
+
3760
+ disconnectedCallback() {
3761
+ window.removeEventListener("resize", this.#refreshToolbarOverflow);
3762
+ }
3763
+
3749
3764
  setEditor(editorElement) {
3750
3765
  this.editorElement = editorElement;
3751
3766
  this.editor = editorElement.editor;
@@ -3810,13 +3825,12 @@ class LexicalToolbarElement extends HTMLElement {
3810
3825
  event.shiftKey ? 'shift' : null,
3811
3826
  ].filter(Boolean);
3812
3827
 
3813
- return [...modifiers, pressedKey].join('+')
3828
+ return [ ...modifiers, pressedKey ].join('+')
3814
3829
  }
3815
3830
 
3816
3831
  #assignButtonTabindex() {
3817
3832
  const baseTabIndex = parseInt(this.editorElement.editorContentElement.getAttribute("tabindex") ?? "0");
3818
- const buttons = this.querySelectorAll("button");
3819
- buttons.forEach((button, index) => {
3833
+ this.#buttons.forEach((button, index) => {
3820
3834
  button.setAttribute("tabindex", `${baseTabIndex + index + 1}`);
3821
3835
  });
3822
3836
  }
@@ -3895,19 +3909,64 @@ class LexicalToolbarElement extends HTMLElement {
3895
3909
  }
3896
3910
  }
3897
3911
 
3912
+ #toolbarIsOverflowing() {
3913
+ return this.scrollWidth > this.clientWidth
3914
+ }
3915
+
3916
+ #refreshToolbarOverflow = () => {
3917
+ this.#resetToolbar();
3918
+ this.#compactMenu();
3919
+
3920
+ this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
3921
+ }
3922
+
3923
+ get #overflow() {
3924
+ return this.querySelector(".lexxy-editor__toolbar-overflow")
3925
+ }
3926
+
3927
+ get #overflowMenu() {
3928
+ return this.querySelector(".lexxy-editor__toolbar-overflow-menu")
3929
+ }
3930
+
3931
+ #resetToolbar() {
3932
+ while (this.#overflowMenu.children.length > 0) {
3933
+ this.insertBefore(this.#overflowMenu.children[0], this.#overflow);
3934
+ }
3935
+ }
3936
+
3937
+ #compactMenu() {
3938
+ const buttons = this.#buttons.reverse();
3939
+ let movedToOverflow = false;
3940
+
3941
+ for (const button of buttons) {
3942
+ if (this.#toolbarIsOverflowing()) {
3943
+ this.#overflowMenu.appendChild(button);
3944
+ movedToOverflow = true;
3945
+ } else {
3946
+ if (movedToOverflow) this.#overflowMenu.appendChild(button);
3947
+ break
3948
+ }
3949
+ }
3950
+ }
3951
+
3952
+ get #buttons() {
3953
+ return Array.from(this.querySelectorAll(":scope > button"))
3954
+ }
3955
+
3898
3956
  static get defaultTemplate() {
3899
3957
  return `
3900
- <button type="button" name="bold" data-command="bold" title="Bold">
3901
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="m4.1 23c-.5 0-.7-.4-.7-.7v-20.6c0-.4.4-.7.7-.7h8.9c2 0 3.8.6 4.9 1.5 1.2 1 1.8 2.4 1.8 4.1s-.9 3.2-2.3 4.1c-.2 0-.3.3-.3.5s0 .4.3.5c1.9.8 3.2 2.7 3.2 5s-.7 3.6-2.1 4.7-3.3 1.7-5.6 1.7h-8.8zm4.2-18.1v5.1h3c1.2 0 2-.3 2.7-.7.6-.5.9-1.1.9-1.9s-.3-1.4-.8-1.8-1.3-.6-2.3-.6-2.4 0-3.5 0zm0 8.5v5.8h3.7c1.3 0 2.2-.3 2.8-.7s.9-1.2.9-2.2-.4-1.7-1-2.1-1.7-.7-2.9-.7-2.4 0-3.5 0z" fill-rule="evenodd"/> </svg>
3958
+ <button class="lexxy-editor__toolbar-button" type="button" name="bold" data-command="bold" title="Bold">
3959
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 22V2h8.183c1.764 0 3.174.435 4.228 1.304 1.055.87 1.582 2.076 1.582 3.62 0 .8-.148 1.503-.445 2.109a3.94 3.94 0 01-1.194 1.465 4.866 4.866 0 01-1.726.806v.176c.786.078 1.51.312 2.172.703a4.293 4.293 0 011.596 1.627c.403.693.604 1.543.604 2.549 0 1.192-.292 2.207-.877 3.048-.585.84-1.39 1.484-2.416 1.934-1.026.44-2.206.659-3.538.659H5zM8.854 4.974v5.348h2.56c.873 0 1.582-.107 2.129-.322.556-.215.963-.523 1.222-.923.269-.41.403-.904.403-1.48 0-.82-.254-1.46-.762-1.92-.499-.468-1.204-.703-2.115-.703H8.854zm0 8.103v5.949h2.877c1.534 0 2.636-.245 3.307-.733.671-.498 1.007-1.221 1.007-2.168 0-.635-.134-1.178-.403-1.627-.268-.459-.666-.81-1.193-1.055-.518-.244-1.156-.366-1.913-.366H8.854z"/></svg>
3902
3960
  </button>
3903
-
3904
- <button type="button" name="italic" data-command="italic" title="Italic">
3905
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18.3 1.7c-1.7.4-2.5 1.3-2.7 2.6l-2.4 15.6c-.2 1.1.5 2.1 1.6 2.4.2 0 .2.2.2.3v.7h-.1c0 .1-.2.3-.3.3h-9c-.2 0-.3-.1-.3-.3v-.7h.1c0-.1.1-.2.2-.2 1.7-.4 2.5-1.3 2.7-2.6l2.4-15.6c.2-1.1-.5-2.1-1.6-2.4-.2 0-.2-.2-.2-.3v-.7h.1c0-.1.2-.3.3-.3h9c.2 0 .3.1.3.3v.7h-.1c0 .1-.1.2-.2.2z" fill-rule="evenodd"/></svg>
3961
+
3962
+ <button class="lexxy-editor__toolbar-button" type="button" name="italic" data-command="italic" title="Italic">
3963
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.1 4h-1.5l-3.2 16h1.5l-.4 2h-7l.4-2h1.5l3.2-16h-1.5l.4-2h7l-.4 2z"/></svg>
3906
3964
  </button>
3907
-
3908
- <button type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
3909
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22.2 1.8c-1.1-1.2-2.5-1.8-4.3-1.8s-3.1.6-4.4 1.8l-4.4 4.4c-1.2 1.2-1.8 2.7-1.8 4.3s.6 3.1 1.8 4.3h.2c.2.3.5.4 1 .4.7 0 1.1-.3 1.2-.6.3-.3.4-.7.4-1.1s-.2-.8-.5-1.2c-.5-.5-.8-1.2-.8-1.9s.3-1.4.8-1.9l4.4-4.2c.6-.5 1.5-.8 2.1-.8s1.4.2 1.9.7.8 1.2.8 1.9-.3 1.4-.8 1.9l-2.6 2.2c-.3.3-.5.7-.5 1.2s.2.8.5 1.2c.8.6 1.7.6 2.4 0l2.6-2.2c2.4-2.4 2.4-6.1 0-8.5z"/><path d="m12.3 9.3c-.3.3-.5.8-.5 1.3 0 .4.2.8.5 1.1.5.5.8 1.2.8 1.9s-.3 1.4-.9 1.9l-4.4 4.4c-.4.4-1.2.7-1.8.7s-1.4-.2-1.9-.7-.8-1.2-.8-1.9.3-1.4.8-1.9l2.5-2.4c.7-.7.7-1.7 0-2.4-.8-.6-1.7-.6-2.4 0l-2.5 2.4c-1 1.1-1.7 2.6-1.7 4.2s.6 3.1 1.8 4.3c1.3 1.2 2.7 1.8 4.2 1.8s3.2-.7 4.3-1.8l4.4-4.4c2.4-2.4 2.4-6.1 0-8.6-.8-.6-1.7-.6-2.4 0z"/></svg>
3965
+
3966
+ <button class="lexxy-editor__toolbar-button" type="button" name="link" title="Link" data-dialog-target="link-dialog" data-hotkey="cmd+k ctrl+k">
3967
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.111 9.546a1.5 1.5 0 012.121 0 5.5 5.5 0 010 7.778l-2.828 2.828a5.5 5.5 0 01-7.778 0 5.498 5.498 0 010-7.777l2.828-2.83a1.5 1.5 0 01.355-.262 6.52 6.52 0 00.351 3.799l-1.413 1.414a2.499 2.499 0 000 3.535 2.499 2.499 0 003.535 0l2.83-2.828a2.5 2.5 0 000-3.536 1.5 1.5 0 010-2.121z"/><path d="M12.111 3.89a5.5 5.5 0 117.778 7.777l-2.828 2.829a1.496 1.496 0 01-.355.262 6.522 6.522 0 00-.351-3.8l1.413-1.412a2.5 2.5 0 10-3.536-3.535l-2.828 2.828a2.5 2.5 0 000 3.536 1.5 1.5 0 01-2.122 2.12 5.5 5.5 0 010-7.777l2.83-2.829z"/></svg>
3910
3968
  </button>
3969
+
3911
3970
  <lexxy-link-dialog class="lexxy-link-dialog">
3912
3971
  <dialog id="link-dialog" closedby="any">
3913
3972
  <form method="dialog">
@@ -3916,33 +3975,38 @@ class LexicalToolbarElement extends HTMLElement {
3916
3975
  <button type="submit" class="btn" value="link">Link</button>
3917
3976
  <button type="button" class="btn" value="unlink">Unlink</button>
3918
3977
  </div>
3919
- </form>
3920
- </dialog>
3978
+ </form>
3979
+ </dialog>
3921
3980
  </lexxy-link-dialog>
3922
-
3923
- <button type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
3924
- <svg viewBox="0 0 24 22" xmlns="http://www.w3.org/2000/svg"> <path d="m1.1 5.2c.6-.7 1.4-1.3 2.4-1.4 2.6-.4 4.2.4 5.3 1.9 2 2.3 1.9 5.1.6 7.6-1.3 2.4-4 4.6-7.2 5.1-.4 0-.7-.1-1-.4-.1-.3-.1-.7.3-1.1l1.1-1.1c.3-.4.6-.7.7-1.1s.3-.9 0-1.3c0-.4-.6-.7-1-1-1.2-.8-2.3-2.2-2.3-4.1.1-1.4.4-2.4 1.1-3.1z"/> <path d="m14.6 5.2c.6-.7 1.6-1.1 2.6-1.4 2.4-.4 4.2.4 5.3 1.9 2 2.3 1.9 5.1.6 7.6-1.3 2.4-4 4.6-7.2 5.1-.4 0-.7-.1-1-.4-.1-.3-.1-.7.3-1.1l1.1-1.1c.3-.4.6-.7.7-1.1s.3-.9 0-1.3c-.1-.4-.6-.7-1-1-1.3-.6-2.4-2-2.4-3.9s.4-2.6 1-3.3z"/> </svg>
3981
+
3982
+ <button class="lexxy-editor__toolbar-button" type="button" name="quote" data-command="insertQuoteBlock" title="Quote">
3983
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.5 5C8.985 5 11 7.09 11 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.022.746a.563.563 0 01-.52-.36.602.602 0 01.067-.57l.055-.066.009-.009.041-.048a4.25 4.25 0 00.168-.21c.143-.188.336-.47.53-.84a6.743 6.743 0 00.75-2.605C3.705 13.994 2 12.038 2 9.667 2 7.089 4.015 5 6.5 5zM17.5 5C19.985 5 22 7.09 22 9.667c0 2.694-.962 5.005-2.187 6.644-.613.82-1.3 1.481-1.978 1.943-.668.454-1.375.746-2.023.746a.563.563 0 01-.52-.36.602.602 0 01.068-.57l.055-.066.009-.009.041-.048c.039-.045.097-.115.168-.21a6.16 6.16 0 00.53-.84 6.745 6.745 0 00.75-2.605C14.705 13.994 13 12.038 13 9.667 13 7.089 15.015 5 17.5 5z"/></svg>
3925
3984
  </button>
3926
-
3927
- <button type="button" name="heading" data-command="rotateHeadingFormat" title="Heading">
3928
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m5.7 6.2v16.3h3.8v-16.3h5.7v-3.8h-15.2v3.8zm10.1 16.4h3.8v-8.8h4.4v-3.8h-12.6v3.8h4.4z" fill-rule="evenodd"/></svg>
3985
+
3986
+ <button class="lexxy-editor__toolbar-button" type="button" name="heading" data-command="rotateHeadingFormat" title="Heading">
3987
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15.322 5.315H9.64V22H5.684V5.315H0v-3.31h15.322v3.31z"/><path d="M23.957 11.79H19.92V22h-3.402V11.79H12.48V9.137h11.477v2.653z"/></svg>
3929
3988
  </button>
3930
-
3931
- <button type="button" name="code" data-command="insertCodeBlock" title="Code">
3932
- <svg viewBox="0 0 24 22" xmlns="http://www.w3.org/2000/svg"><path d="m8 4.7c-.6-.6-1.6-.6-2.4 0l-5.1 5.2c-.3.3-.5.7-.5 1.2s.2.9.5 1.2l5.1 5.1c.3.3.7.5 1.2.5s.9-.2 1.2-.5c.6-.6.6-1.7 0-2.4l-4-4 4-4c.6-.6.6-1.7 0-2.4z"/><path d="m23.5 9.9-5.1-5.1c-.6-.6-1.8-.6-2.4 0-.3.3-.5.7-.5 1.2s.2.9.5 1.2l4 4-4 4c-.3.3-.5.7-.5 1.2s.2.9.5 1.2c.3.2.7.4 1.1.4s.9-.2 1.2-.5l5.1-5.1c.3-.3.5-.7.5-1.2s-.2-.9-.5-1.2z"/></svg>
3989
+
3990
+ <button class="lexxy-editor__toolbar-button" type="button" name="code" data-command="insertCodeBlock" title="Code">
3991
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.121 6l-6 6 6 6-2.12 2.121-7.061-7.06a1.5 1.5 0 010-2.121L8 3.879 10.121 6zM23.06 10.94a1.5 1.5 0 010 2.12L16 20.121 13.88 18l6-6-6-6L16 3.879l7.06 7.06z"/></svg>
3933
3992
  </button>
3934
-
3935
- <button type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
3936
- <svg viewBox="0 0 24 22" xmlns="http://www.w3.org/2000/svg"> <path d="m2.1 4.8c1.1 0 2.1-.9 2.1-2.1s-1-2-2.1-2-2.1.9-2.1 2.1.9 2 2.1 2zm4.1-2c0-.8.6-1.4 1.4-1.4h15.1c.7 0 1.3.6 1.3 1.4s-.6 1.4-1.4 1.4h-15.1c-.7 0-1.3-.7-1.3-1.4zm1.3 6.8c-.8 0-1.4.6-1.4 1.4s.6 1.4 1.4 1.4h15.1c.8 0 1.4-.6 1.4-1.4s-.6-1.4-1.4-1.4zm0 8.3c-.8 0-1.4.6-1.4 1.4s.6 1.4 1.4 1.4h15.1c.8 0 1.4-.6 1.4-1.4s-.6-1.4-1.4-1.4zm-3.4-6.9c0 1.1-.9 2.1-2.1 2.1s-2-1-2-2.1.9-2.1 2.1-2.1 2 1 2 2.1zm-2 10.3c1.1 0 2.1-.9 2.1-2.1s-.9-2.1-2.1-2.1-2.1 1-2.1 2.1.9 2.1 2.1 2.1z" fill-rule="evenodd"/> </svg>
3993
+
3994
+ <button class="lexxy-editor__toolbar-button" type="button" name="unordered-list" data-command="insertUnorderedList" title="Bullet list">
3995
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5 5a2 2 0 11-4 0 2 2 0 014 0zM5 12a2 2 0 11-4 0 2 2 0 014 0zM5 19a2 2 0 11-4 0 2 2 0 014 0zM7 5.25C7 4.56 7.56 4 8.25 4h13.5a1.25 1.25 0 110 2.5H8.25C7.56 6.5 7 5.94 7 5.25zM7 12.25c0-.69.56-1.25 1.25-1.25h13.5a1.25 1.25 0 110 2.5H8.25c-.69 0-1.25-.56-1.25-1.25zM7 19.25c0-.69.56-1.25 1.25-1.25h13.5a1.25 1.25 0 110 2.5H8.25c-.69 0-1.25-.56-1.25-1.25z"/></svg>
3937
3996
  </button>
3938
-
3939
- <button type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
3940
- <svg viewBox="0 0 24 22" xmlns="http://www.w3.org/2000/svg"><path d="m6.7 3c0-.7.6-1.3 1.3-1.3h14.7c.7 0 1.3.6 1.3 1.3s-.6 1.3-1.3 1.3h-14.7c-.7 0-1.3-.6-1.3-1.3zm1.3 6.7c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h14.7c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3zm0 8c-.7 0-1.3.6-1.3 1.3s.6 1.3 1.3 1.3h14.7c.7 0 1.3-.6 1.3-1.3s-.6-1.3-1.3-1.3z" fill-rule="evenodd"/><path d="m1.5 19.6v-.9h.5c.4 0 .8-.3.8-.7s-.3-.7-.8-.7-.8.3-.8.7h-1.2c0-.9.8-1.6 2-1.6s2 .5 2 1.5-.4 1.1-1.1 1.2c.7 0 1.2.7 1.2 1.3 0 1.1-1.1 1.6-2.1 1.6s-2-.8-2-1.6h1.2c0 .4.4.7.8.7s.8-.3.8-.7-.3-.7-.8-.7h-.7.1zm0-9.7h-1.2c0-.9.7-1.7 2-1.7s2 .7 2 1.6-.5 1.2-.9 1.7l-1.1 1.2h2.1v1.1h-3.9v-.8l2-2c.3-.3.5-.7.5-1.1s-.3-.7-.7-.7-.7.3-.7.7h-.3zm1.7-4.4h-1.3v-4.3l-1.2.8v-1.2l1.3-.8h1.3v5.5z"/></svg>
3997
+
3998
+ <button class="lexxy-editor__toolbar-button" type="button" name="ordered-list" data-command="insertOrderedList" title="Numbered list">
3999
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 5.25C7 4.56 7.56 4 8.25 4h13.5a1.25 1.25 0 110 2.5H8.25C7.56 6.5 7 5.94 7 5.25zM7 12.25c0-.69.56-1.25 1.25-1.25h13.5a1.25 1.25 0 110 2.5H8.25c-.69 0-1.25-.56-1.25-1.25zM7 19.25c0-.69.56-1.25 1.25-1.25h13.5a1.25 1.25 0 110 2.5H8.25c-.69 0-1.25-.56-1.25-1.25zM4.438 8H3.39V3.684H3.34c-.133.093-.267.188-.402.285l-.407.289a129.5 129.5 0 00-.402.285v-.969l.633-.453c.21-.15.42-.302.629-.453h1.046V8zM2.672 11.258h-1v-.051c0-.206.036-.405.11-.598.075-.195.188-.37.34-.527.15-.156.339-.281.566-.375.229-.094.498-.14.808-.14.367 0 .688.065.961.195s.484.308.633.535c.15.224.226.478.226.762 0 .244-.046.463-.14.656-.091.19-.209.368-.352.535-.14.164-.289.332-.445.504L3.168 14.09v.05h2.238V15H1.723v-.656l1.949-2.102c.096-.101.19-.207.281-.316.091-.112.167-.232.227-.36a.953.953 0 00.09-.41.712.712 0 00-.387-.648.845.845 0 00-.41-.098.81.81 0 00-.43.11.75.75 0 00-.277.293.824.824 0 00-.094.386V11.258zM2.852 19.66v-.812h.562a.917.917 0 00.43-.098.742.742 0 00.293-.266.673.673 0 00.101-.379.654.654 0 00-.234-.523.87.87 0 00-.59-.2.987.987 0 00-.336.055.837.837 0 00-.258.149.712.712 0 00-.172.215.66.66 0 00-.066.25h-.98c.007-.209.053-.403.136-.582.084-.18.203-.336.36-.469.156-.135.346-.24.57-.316.227-.076.486-.115.777-.118a2.33 2.33 0 01.965.176c.271.12.48.285.63.496.15.209.227.448.23.719a1.11 1.11 0 01-.16.637 1.28 1.28 0 01-.825.586v.054c.162.016.33.07.504.164.177.094.328.232.453.415.125.18.189.411.192.695a1.37 1.37 0 01-.157.676c-.104.197-.25.365-.437.503-.188.136-.404.24-.649.313-.242.07-.5.105-.777.105-.401 0-.743-.067-1.027-.203a1.608 1.608 0 01-.649-.547 1.46 1.46 0 01-.238-.75h.969c.01.128.057.243.14.344a.885.885 0 00.332.238c.141.058.3.088.477.09.195 0 .366-.034.512-.101a.798.798 0 00.336-.29.744.744 0 00.117-.425.74.74 0 00-.446-.695 1.082 1.082 0 00-.496-.106h-.59z"/></svg>
3941
4000
  </button>
3942
-
3943
- <button type="button" name="upload" data-command="uploadAttachments" title="Upload file">
3944
- <svg viewBox="0 0 24 20" xmlns="http://www.w3.org/2000/svg"> <path d="m22 20h-20c-1.1 0-2-.9-2-2.1v-15.8c0-1.2.9-2.1 2-2.1h20c1.1 0 2 .9 2 2.1v15.8c0 1.1-.9 2.1-2 2.1zm0-2.9v-14.5c0-.3-.2-.5-.5-.5h-19c-.3 0-.5.2-.5.5v14.5c0 .1.1.2.2.2s.2 0 .2-.1l2.2-3.3c.1-.2.3-.3.5-.3h.7l2.6-4c.1-.2.3-.3.5-.3h.7c.2 0 .4.1.5.3l5.3 8c0 .1.2.2.3.2h.3c.2 0 .4-.2.4-.4s0-.2 0-.2l-1.3-1.9c-.2-.2-.2-.6 0-.8l1.2-1.6c.1-.2.3-.3.5-.3h1.1c.2 0 .4 0 .5.3l3.2 4.4c0 .1.3.2.4 0 .2 0 .2 0 .2-.2zm-5.5-7.6c-1.4 0-2.5-1.2-2.5-2.6s1.1-2.6 2.5-2.6 2.5 1.2 2.5 2.6-1.1 2.6-2.5 2.6z" fill-rule="evenodd"/> </svg>
4001
+
4002
+ <button class="lexxy-editor__toolbar-button" type="button" name="upload" data-command="uploadAttachments" title="Upload file">
4003
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 8a2 2 0 110 4 2 2 0 010-4z""/><path d="M22 2a1 1 0 011 1v18a1 1 0 01-1 1H2a1 1 0 01-1-1V3a1 1 0 011-1h20zM3 18.714L9 11l5.25 6.75L17 15l4 4V4H3v14.714z"/></svg>
3945
4004
  </button>
4005
+
4006
+ <details class="lexxy-editor__toolbar-overflow">
4007
+ <summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
4008
+ <div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
4009
+ </details>
3946
4010
  `
3947
4011
  }
3948
4012
  }
@@ -5603,7 +5667,7 @@ class ActionTextAttachmentNode extends gi {
5603
5667
  }
5604
5668
 
5605
5669
  #select(figure) {
5606
- dispatchCustomEvent(figure, "lexxy:node-selected", { key: this.getKey() });
5670
+ dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
5607
5671
  }
5608
5672
 
5609
5673
  #createEditableCaption() {
@@ -5635,13 +5699,13 @@ class ActionTextAttachmentNode extends gi {
5635
5699
  }
5636
5700
 
5637
5701
  #updateCaptionValueFromInput(input) {
5638
- dispatchCustomEvent(input, "lexxy:node-invalidated", { key: this.getKey(), values: { caption: input.value } });
5702
+ dispatchCustomEvent(input, "lexxy:internal:invalidate-node", { key: this.getKey(), values: { caption: input.value } });
5639
5703
  }
5640
5704
 
5641
5705
  #handleCaptionInputKeydown(event) {
5642
5706
  if (event.key === "Enter") {
5643
5707
  this.#updateCaptionValueFromInput(event.target);
5644
- dispatchCustomEvent(event.target, "lexxy:move-to-next-line");
5708
+ dispatchCustomEvent(event.target, "lexxy:internal:move-to-next-line");
5645
5709
  event.preventDefault();
5646
5710
  }
5647
5711
  event.stopPropagation();
@@ -6199,7 +6263,7 @@ class Selection {
6199
6263
  }
6200
6264
 
6201
6265
  #listenForNodeSelections() {
6202
- this.editor.getRootElement().addEventListener("lexxy:node-selected", async (event) => {
6266
+ this.editor.getRootElement().addEventListener("lexxy:internal:select-node", async (event) => {
6203
6267
  await nextFrame();
6204
6268
 
6205
6269
  const { key } = event.detail;
@@ -6214,7 +6278,7 @@ class Selection {
6214
6278
  });
6215
6279
  });
6216
6280
 
6217
- this.editor.getRootElement().addEventListener("lexxy:move-to-next-line", (event) => {
6281
+ this.editor.getRootElement().addEventListener("lexxy:internal:move-to-next-line", (event) => {
6218
6282
  this.#selectOrAppendNextLine();
6219
6283
  });
6220
6284
  }
@@ -6288,7 +6352,7 @@ class Selection {
6288
6352
  if (ur(selection)) {
6289
6353
  return this.#getTopLevelFromNodeSelection(selection)
6290
6354
  }
6291
-
6355
+
6292
6356
  if (cr(selection)) {
6293
6357
  return this.#getTopLevelFromRangeSelection(selection)
6294
6358
  }
@@ -6367,7 +6431,7 @@ class Selection {
6367
6431
 
6368
6432
  #getReliableRectFromRange(range) {
6369
6433
  let rect = range.getBoundingClientRect();
6370
-
6434
+
6371
6435
  if (this.#isRectUnreliable(rect)) {
6372
6436
  const marker = this.#createAndInsertMarker(range);
6373
6437
  rect = marker.getBoundingClientRect();
@@ -6424,12 +6488,12 @@ class Selection {
6424
6488
  const nativeSelection = window.getSelection();
6425
6489
  const anchorNode = nativeSelection.anchorNode;
6426
6490
  const parentElement = this.#getElementFromNode(anchorNode);
6427
-
6491
+
6428
6492
  if (parentElement instanceof HTMLElement) {
6429
6493
  const computed = window.getComputedStyle(parentElement);
6430
6494
  return parseFloat(computed.fontSize)
6431
6495
  }
6432
-
6496
+
6433
6497
  return 0
6434
6498
  }
6435
6499
 
@@ -6709,6 +6773,10 @@ class Contents {
6709
6773
  return
6710
6774
  }
6711
6775
 
6776
+ if (!this.#shouldUploadFile(file)) {
6777
+ return
6778
+ }
6779
+
6712
6780
  const uploadUrl = this.editorElement.directUploadUrl;
6713
6781
  const blobUrlTemplate = this.editorElement.blobUrlTemplate;
6714
6782
 
@@ -6997,6 +7065,10 @@ class Contents {
6997
7065
  }
6998
7066
  }
6999
7067
  }
7068
+
7069
+ #shouldUploadFile(file) {
7070
+ return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
7071
+ }
7000
7072
  }
7001
7073
 
7002
7074
  /**
@@ -7213,7 +7285,7 @@ class CustomActionTextAttachmentNode extends gi {
7213
7285
  const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
7214
7286
 
7215
7287
  figure.addEventListener("click", (event) => {
7216
- dispatchCustomEvent(figure, "lexxy:node-selected", { key: this.getKey() });
7288
+ dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
7217
7289
  });
7218
7290
 
7219
7291
  figure.insertAdjacentHTML("beforeend", this.innerHtml);
@@ -7264,7 +7336,7 @@ class LexicalEditorElement extends HTMLElement {
7264
7336
  constructor() {
7265
7337
  super();
7266
7338
  this.internals = this.attachInternals();
7267
- this.internals.role = "textbox";
7339
+ this.internals.role = "presentation";
7268
7340
  }
7269
7341
 
7270
7342
  connectedCallback() {
@@ -7276,6 +7348,8 @@ class LexicalEditorElement extends HTMLElement {
7276
7348
 
7277
7349
  CommandDispatcher.configureFor(this);
7278
7350
  this.#initialize();
7351
+
7352
+ requestAnimationFrame(() => dispatch(this, "lexxy:initialize"));
7279
7353
  this.toggleAttribute("connected", true);
7280
7354
 
7281
7355
  this.valueBeforeDisconnect = null;
@@ -7416,12 +7490,20 @@ class LexicalEditorElement extends HTMLElement {
7416
7490
  }
7417
7491
 
7418
7492
  #createEditorContentElement() {
7419
- const editorContentElement = createElement("div", { classList: "lexxy-editor__content", contenteditable: true, placeholder: this.getAttribute("placeholder") });
7493
+ const editorContentElement = createElement("div", {
7494
+ classList: "lexxy-editor__content",
7495
+ contenteditable: true,
7496
+ role: "textbox",
7497
+ "aria-multiline": true,
7498
+ "aria-label": this.#labelText,
7499
+ placeholder: this.getAttribute("placeholder")
7500
+ });
7420
7501
  editorContentElement.id = `${this.id}-content`;
7502
+ this.#ariaAttributes.forEach(attribute => editorContentElement.setAttribute(attribute.name, attribute.value));
7421
7503
  this.appendChild(editorContentElement);
7422
7504
 
7423
7505
  if (this.getAttribute("tabindex")) {
7424
- this.editorContentElement.setAttribute("tabindex", this.getAttribute("tabindex"));
7506
+ editorContentElement.setAttribute("tabindex", this.getAttribute("tabindex"));
7425
7507
  this.removeAttribute("tabindex");
7426
7508
  } else {
7427
7509
  editorContentElement.setAttribute("tabindex", 0);
@@ -7430,6 +7512,14 @@ class LexicalEditorElement extends HTMLElement {
7430
7512
  return editorContentElement
7431
7513
  }
7432
7514
 
7515
+ get #labelText() {
7516
+ return Array.from(this.internals.labels).map(label => label.textContent).join(" ")
7517
+ }
7518
+
7519
+ get #ariaAttributes() {
7520
+ return Array.from(this.attributes).filter(attribute => attribute.name.startsWith("aria-"))
7521
+ }
7522
+
7433
7523
  set #internalFormValue(html) {
7434
7524
  const changed = this.#internalFormValue !== undefined && this.#internalFormValue !== this.value;
7435
7525
 
@@ -7492,7 +7582,7 @@ class LexicalEditorElement extends HTMLElement {
7492
7582
  }
7493
7583
 
7494
7584
  #listenForInvalidatedNodes() {
7495
- this.editor.getRootElement().addEventListener("lexxy:node-invalidated", (event) => {
7585
+ this.editor.getRootElement().addEventListener("lexxy:internal:invalidate-node", (event) => {
7496
7586
  const { key, values } = event.detail;
7497
7587
 
7498
7588
  this.editor.update(() => {
Binary file
Binary file