lexxy 0.1.9.beta → 0.1.10.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: 7b78b5bf9021bb45f381a3da1879ca0146f3084091b7ac21b6b49dd7b47f0e6b
4
- data.tar.gz: b123e13e32adff1f847a6914789f8f97c86798954c861197feb3cf9c5549dadc
3
+ metadata.gz: 2fffdc9d43469a6366a3d6fa16b6e5b8284c5a19bdcfa3cbffeb79a96cf16970
4
+ data.tar.gz: b1e76d677c2c7e9c55ff713eba32c89f04cc0e267153f4e4def7cf48238dc733
5
5
  SHA512:
6
- metadata.gz: a30b935d910971f32acd1f95aec23624807206182f030f82f94c365eb024ba34c970daecd95e03787d1d78feab663554e48599d008e3b1ecae3d790fc7ef869f
7
- data.tar.gz: 41b4ae9469a19335dda3eb1584aaa12b362aa34c4cb5957a5220b5cb17f49f481c5e065be6a98f8419e72a7d1951b66557b183745ee6ea965b1221a0207cf14d
6
+ metadata.gz: 0c7752a992efceb4f40f411bb154037a21b7ffeb83db7dc28bf882f68ed2ff57f21ebe4c1d61dc420d6b344fd45fb8d819595a753e33d90134a8e3536d6fd0ba
7
+ data.tar.gz: 761ce9eee667c72a4cab67a5572cad3013f468c9248f415477c144b640a26852400c07afcdbe287afe492f858d54a798281ac96c89814fd69683f3ecd4488d77
data/README.md CHANGED
@@ -341,9 +341,84 @@ The sandbox app is available at http://localhost:3000. There is also a CRUD exam
341
341
 
342
342
  ## Events
343
343
 
344
- * `lexxy:initialize`: Fired whenever the `<lexxy-editor>` element is attached to the DOM and is ready for use.
345
- * `lexxy:change`: Fired whenever the editor content changes.
346
- * `lexxy:file-accept`: Fired whenever a file is dropped or inserted into the editor. You can access the `File` object through the `event.detail.file` property. Call `preventDefault` on the event to cancel upload and prevent attaching the file.
344
+ Lexxy fires a handful of custom events that you can hook into.
345
+ Each event is dispatched on the `<lexxy-editor>` element.
346
+
347
+ ### `lexxy:initialize`
348
+
349
+ Fired when the `<lexxy-editor>` element is attached to the DOM and ready for use.
350
+ This is useful for one-time setup.
351
+
352
+ ### `lexxy:change`
353
+
354
+ Fired whenever the editor content changes.
355
+ You can use this to sync the editor state with your application.
356
+
357
+ ### `lexxy:file-accept`
358
+
359
+ Fired when a file is dropped or inserted into the editor.
360
+
361
+ - Access the file via `event.detail.file`.
362
+ - Call `event.preventDefault()` to cancel the upload and prevent attaching the file.
363
+
364
+ ### `lexxy:insert-link`
365
+
366
+ Fired when a plain text link is pasted into the editor.
367
+ Access the link’s URL via `event.detail.url`.
368
+
369
+ You also get a handful of callback helpers on `event.detail`:
370
+
371
+ - **`replaceLinkWith(html, options)`** – replace the pasted link with your own HTML.
372
+ - **`insertBelowLink(html, options)`** – insert custom HTML below the link.
373
+ - **Attachment rendering** – pass `{ attachment: true }` in `options` to render as non-editable content,
374
+ or `{ attachment: { sgid: "your-sgid-here" } }` to provide a custom SGID.
375
+
376
+ #### Example: Link Unfurling with Stimulus
377
+
378
+ When a user pastes a link, you may want to turn it into a preview or embed.
379
+ Here’s a Stimulus controller that sends the URL to your app, retrieves metadata,
380
+ and replaces the plain text link with a richer version:
381
+
382
+ ```javascript
383
+ // app/javascript/controllers/link_unfurl_controller.js
384
+ import { Controller } from "@hotwired/stimulus"
385
+ import { post } from "@rails/request.js"
386
+
387
+ export default class extends Controller {
388
+ static values = {
389
+ url: String, // endpoint that handles unfurling
390
+ }
391
+
392
+ unfurl(event) {
393
+ this.#unfurlLink(event.detail.url, event.detail)
394
+ }
395
+
396
+ async #unfurlLink(url, callbacks) {
397
+ const { response } = await post(this.urlValue, {
398
+ body: JSON.stringify({ url }),
399
+ headers: {
400
+ "Content-Type": "application/json",
401
+ "Accept": "application/json"
402
+ }
403
+ })
404
+
405
+ const metadata = await response.json()
406
+ this.#insertUnfurledLink(metadata, callbacks)
407
+ }
408
+
409
+ #insertUnfurledLink(metadata, callbacks) {
410
+ // Replace the pasted link with your custom HTML
411
+ callbacks.replaceLinkWith(this.#renderUnfurledLinkHTML(metadata))
412
+
413
+ // Or, insert below the link as an attachment:
414
+ // callbacks.insertBelowLink(this.#renderUnfurledLinkHTML(metadata), { attachment: true })
415
+ }
416
+
417
+ #renderUnfurledLinkHTML(link) {
418
+ return `<a href="${link.canonical_url}">${link.title}</a>`
419
+ }
420
+ }
421
+ ```
347
422
 
348
423
  ## Contributing
349
424
 
@@ -6589,6 +6589,105 @@ class Selection {
6589
6589
  }
6590
6590
  }
6591
6591
 
6592
+ class CustomActionTextAttachmentNode extends gi {
6593
+ static getType() {
6594
+ return "custom_action_text_attachment"
6595
+ }
6596
+
6597
+ static clone(node) {
6598
+ return new CustomActionTextAttachmentNode({ ...node }, node.__key)
6599
+ }
6600
+
6601
+ static importJSON(serializedNode) {
6602
+ return new CustomActionTextAttachmentNode({ ...serializedNode })
6603
+ }
6604
+
6605
+ static importDOM() {
6606
+ return {
6607
+ "action-text-attachment": (attachment) => {
6608
+ const content = attachment.getAttribute("content");
6609
+ if (!attachment.getAttribute("content")) {
6610
+ return null
6611
+ }
6612
+
6613
+ return {
6614
+ conversion: () => {
6615
+ // Preserve initial space if present since Lexical removes it
6616
+ const nodes = [];
6617
+ const previousSibling = attachment.previousSibling;
6618
+ if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
6619
+ nodes.push(Xn(" "));
6620
+ }
6621
+
6622
+ nodes.push(new CustomActionTextAttachmentNode({
6623
+ sgid: attachment.getAttribute("sgid"),
6624
+ innerHtml: JSON.parse(content),
6625
+ contentType: attachment.getAttribute("content-type")
6626
+ }));
6627
+
6628
+ nodes.push(Xn(" "));
6629
+
6630
+ return { node: nodes }
6631
+ },
6632
+ priority: 2
6633
+ }
6634
+ }
6635
+ }
6636
+ }
6637
+
6638
+ constructor({ sgid, contentType, innerHtml }, key) {
6639
+ super(key);
6640
+
6641
+ this.sgid = sgid;
6642
+ this.contentType = contentType || "application/vnd.actiontext.unknown";
6643
+ this.innerHtml = innerHtml;
6644
+ }
6645
+
6646
+ createDOM() {
6647
+ const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
6648
+
6649
+ figure.addEventListener("click", (event) => {
6650
+ dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
6651
+ });
6652
+
6653
+ figure.insertAdjacentHTML("beforeend", this.innerHtml);
6654
+
6655
+ return figure
6656
+ }
6657
+
6658
+ updateDOM() {
6659
+ return true
6660
+ }
6661
+
6662
+ isInline() {
6663
+ return true
6664
+ }
6665
+
6666
+ exportDOM() {
6667
+ const attachment = createElement("action-text-attachment", {
6668
+ sgid: this.sgid,
6669
+ content: JSON.stringify(this.innerHtml),
6670
+ "content-type": this.contentType
6671
+ });
6672
+
6673
+ return { element: attachment }
6674
+ }
6675
+
6676
+ exportJSON() {
6677
+ return {
6678
+ type: "custom_action_text_attachment",
6679
+ version: 1,
6680
+ sgid: this.sgid,
6681
+ contentType: this.contentType,
6682
+ innerHtml: this.innerHtml
6683
+ }
6684
+ }
6685
+
6686
+ decorate() {
6687
+ return null
6688
+ }
6689
+ }
6690
+
6592
6691
  class Contents {
6593
6692
  constructor(editorElement) {
6594
6693
  this.editorElement = editorElement;
@@ -6707,6 +6806,24 @@ class Contents {
6707
6806
  });
6708
6807
  }
6709
6808
 
6809
+ createLink(url) {
6810
+ let linkNodeKey = null;
6811
+
6812
+ this.editor.update(() => {
6813
+ const textNode = Xn(url);
6814
+ const linkNode = d$1(url);
6815
+ linkNode.append(textNode);
6816
+
6817
+ const selection = Nr();
6818
+ if (cr(selection)) {
6819
+ selection.insertNodes([linkNode]);
6820
+ linkNodeKey = linkNode.getKey();
6821
+ }
6822
+ });
6823
+
6824
+ return linkNodeKey
6825
+ }
6826
+
6710
6827
  createLinkWithSelectedText(url) {
6711
6828
  if (!this.hasSelectedText()) return
6712
6829
 
@@ -6835,6 +6952,47 @@ class Contents {
6835
6952
  });
6836
6953
  }
6837
6954
 
6955
+ replaceNodeWithHTML(nodeKey, html, options = {}) {
6956
+ this.editor.update(() => {
6957
+ const node = us(nodeKey);
6958
+ if (!node) return
6959
+
6960
+ const selection = Nr();
6961
+ let wasSelected = false;
6962
+
6963
+ if (cr(selection)) {
6964
+ const selectedNodes = selection.getNodes();
6965
+ wasSelected = selectedNodes.includes(node) || selectedNodes.some(n => n.getParent() === node);
6966
+
6967
+ if (wasSelected) {
6968
+ ms(null);
6969
+ }
6970
+ }
6971
+
6972
+ const replacementNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
6973
+ node.replace(replacementNode);
6974
+
6975
+ if (wasSelected) {
6976
+ replacementNode.selectEnd();
6977
+ }
6978
+ });
6979
+ }
6980
+
6981
+ insertHTMLBelowNode(nodeKey, html, options = {}) {
6982
+ this.editor.update(() => {
6983
+ const node = us(nodeKey);
6984
+ if (!node) return
6985
+
6986
+ let previousNode = node;
6987
+ try {
6988
+ previousNode = node.getTopLevelElementOrThrow();
6989
+ } catch {}
6990
+
6991
+ const newNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
6992
+ previousNode.insertAfter(newNode);
6993
+ });
6994
+ }
6995
+
6838
6996
  get #selection() {
6839
6997
  return this.editorElement.selection
6840
6998
  }
@@ -7077,6 +7235,21 @@ class Contents {
7077
7235
  }
7078
7236
  }
7079
7237
 
7238
+ #createCustomAttachmentNodeWithHtml(html, options = {}) {
7239
+ const attachmentConfig = typeof options === 'object' ? options : {};
7240
+
7241
+ return new CustomActionTextAttachmentNode({
7242
+ sgid: attachmentConfig.sgid || null,
7243
+ contentType: "text/html",
7244
+ innerHtml: html
7245
+ })
7246
+ }
7247
+
7248
+ #createHtmlNodeWith(html) {
7249
+ const htmlNodes = h$3(this.editor, parseHtml(html));
7250
+ return htmlNodes[0] || Pi()
7251
+ }
7252
+
7080
7253
  #shouldUploadFile(file) {
7081
7254
  return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
7082
7255
  }
@@ -7204,12 +7377,27 @@ class Clipboard {
7204
7377
  item.getAsString((text) => {
7205
7378
  if (isUrl(text) && this.contents.hasSelectedText()) {
7206
7379
  this.contents.createLinkWithSelectedText(text);
7380
+ } else if (isUrl(text)) {
7381
+ const nodeKey = this.contents.createLink(text);
7382
+ this.#dispatchLinkInsertEvent(nodeKey, { url: text });
7207
7383
  } else {
7208
7384
  this.#pasteMarkdown(text);
7209
7385
  }
7210
7386
  });
7211
7387
  }
7212
7388
 
7389
+ #dispatchLinkInsertEvent(nodeKey, payload) {
7390
+ const linkManipulationMethods = {
7391
+ replaceLinkWith: (html, options) => this.contents.replaceNodeWithHTML(nodeKey, html, options),
7392
+ insertBelowLink: (html, options) => this.contents.insertHTMLBelowNode(nodeKey, html, options)
7393
+ };
7394
+
7395
+ dispatch(this.editorElement, "lexxy:insert-link", {
7396
+ ...payload,
7397
+ ...linkManipulationMethods
7398
+ });
7399
+ }
7400
+
7213
7401
  #pasteMarkdown(text) {
7214
7402
  const html = d(text);
7215
7403
  this.contents.insertHtml(html);
@@ -7242,105 +7430,6 @@ class Clipboard {
7242
7430
  }
7243
7431
  }
7244
7432
 
7245
- class CustomActionTextAttachmentNode extends gi {
7246
- static getType() {
7247
- return "custom_action_text_attachment"
7248
- }
7249
-
7250
- static clone(node) {
7251
- return new CustomActionTextAttachmentNode({ ...node }, node.__key)
7252
- }
7253
-
7254
- static importJSON(serializedNode) {
7255
- return new CustomActionTextAttachmentNode({ ...serializedNode })
7256
- }
7257
-
7258
- static importDOM() {
7259
- return {
7260
- "action-text-attachment": (attachment) => {
7261
- const content = attachment.getAttribute("content");
7262
- if (!attachment.getAttribute("content")) {
7263
- return null
7264
- }
7265
-
7266
- return {
7267
- conversion: () => {
7268
- // Preserve initial space if present since Lexical removes it
7269
- const nodes = [];
7270
- const previousSibling = attachment.previousSibling;
7271
- if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
7272
- nodes.push(Xn(" "));
7273
- }
7274
-
7275
- nodes.push(new CustomActionTextAttachmentNode({
7276
- sgid: attachment.getAttribute("sgid"),
7277
- innerHtml: JSON.parse(content),
7278
- contentType: attachment.getAttribute("content-type")
7279
- }));
7280
-
7281
- nodes.push(Xn(" "));
7282
-
7283
- return { node: nodes }
7284
- },
7285
- priority: 2
7286
- }
7287
- }
7288
- }
7289
- }
7290
-
7291
- constructor({ sgid, contentType, innerHtml }, key) {
7292
- super(key);
7293
-
7294
- this.sgid = sgid;
7295
- this.contentType = contentType || "application/vnd.actiontext.unknown";
7296
- this.innerHtml = innerHtml;
7297
- }
7298
-
7299
- createDOM() {
7300
- const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
7301
-
7302
- figure.addEventListener("click", (event) => {
7303
- dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
7304
- });
7305
-
7306
- figure.insertAdjacentHTML("beforeend", this.innerHtml);
7307
-
7308
- return figure
7309
- }
7310
-
7311
- updateDOM() {
7312
- return true
7313
- }
7314
-
7315
- isInline() {
7316
- return true
7317
- }
7318
-
7319
- exportDOM() {
7320
- const attachment = createElement("action-text-attachment", {
7321
- sgid: this.sgid,
7322
- content: JSON.stringify(this.innerHtml),
7323
- "content-type": this.contentType
7324
- });
7325
-
7326
- return { element: attachment }
7327
- }
7328
-
7329
- exportJSON() {
7330
- return {
7331
- type: "custom_action_text_attachment",
7332
- version: 1,
7333
- sgid: this.sgid,
7334
- contentType: this.contentType,
7335
- innerHtml: this.innerHtml
7336
- }
7337
- }
7338
-
7339
- decorate() {
7340
- return null
7341
- }
7342
- }
7343
-
7344
7433
  class LexicalEditorElement extends HTMLElement {
7345
7434
  static formAssociated = true
7346
7435
  static debug = true
@@ -7575,6 +7664,7 @@ class LexicalEditorElement extends HTMLElement {
7575
7664
  this.cachedValue = null;
7576
7665
  this.#internalFormValue = this.value;
7577
7666
  this.#toggleEmptyStatus();
7667
+ this.#validateRequired();
7578
7668
  }));
7579
7669
  }
7580
7670
 
@@ -7681,6 +7771,14 @@ class LexicalEditorElement extends HTMLElement {
7681
7771
  return !this.editorContentElement.textContent.trim() && !containsVisuallyRelevantChildren(this.editorContentElement)
7682
7772
  }
7683
7773
 
7774
+ #validateRequired() {
7775
+ if (this.hasAttribute("required") && this.#isEmpty) {
7776
+ this.internals.setValidity({ valueMissing: true }, "Please fill out this field.", this.editorContentElement);
7777
+ } else {
7778
+ this.internals.setValidity({});
7779
+ }
7780
+ }
7781
+
7684
7782
  #reset() {
7685
7783
  this.#unregisterHandlers();
7686
7784
 
Binary file
Binary file