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 +4 -4
- data/README.md +78 -3
- data/app/assets/javascript/lexxy.js +197 -99
- data/app/assets/javascript/lexxy.js.br +0 -0
- data/app/assets/javascript/lexxy.js.gz +0 -0
- data/app/assets/javascript/lexxy.min.js +1 -1
- data/app/assets/javascript/lexxy.min.js.br +0 -0
- data/app/assets/javascript/lexxy.min.js.gz +0 -0
- data/lib/lexxy/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2fffdc9d43469a6366a3d6fa16b6e5b8284c5a19bdcfa3cbffeb79a96cf16970
|
4
|
+
data.tar.gz: b1e76d677c2c7e9c55ff713eba32c89f04cc0e267153f4e4def7cf48238dc733
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
345
|
-
|
346
|
-
|
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
|