@37signals/lexxy 0.1.8-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.
- package/README.md +78 -3
- package/dist/lexxy.esm.js +212 -105
- package/package.json +2 -1
package/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
|
|
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import DOMPurify from 'dompurify';
|
|
2
|
-
import { $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, $
|
|
2
|
+
import { $getSelection, $isRangeSelection, $isTextNode, DecoratorNode, $getNodeByKey, HISTORY_MERGE_TAG, FORMAT_TEXT_COMMAND, PASTE_COMMAND, COMMAND_PRIORITY_LOW, $isNodeSelection, $getRoot, $isLineBreakNode, $isElementNode, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, SELECTION_CHANGE_COMMAND, $createNodeSelection, $setSelection, $createParagraphNode, $createTextNode, $isParagraphNode, $insertNodes, $createLineBreakNode, CLEAR_HISTORY_COMMAND, $addUpdateTag, SKIP_DOM_SELECTION_TAG, createEditor, KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, COMMAND_PRIORITY_HIGH, KEY_TAB_COMMAND, KEY_SPACE_COMMAND } from 'lexical';
|
|
3
3
|
import { $isListNode, $isListItemNode, INSERT_UNORDERED_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, ListNode, ListItemNode, registerList } from '@lexical/list';
|
|
4
4
|
import { $isQuoteNode, $isHeadingNode, $createQuoteNode, $createHeadingNode, QuoteNode, HeadingNode, registerRichText } from '@lexical/rich-text';
|
|
5
5
|
import { $isCodeNode, $isCodeHighlightNode, CodeNode, CodeHighlightNode, registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP, normalizeCodeLang } from '@lexical/code';
|
|
6
|
-
import { $isLinkNode, $toggleLink, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
6
|
+
import { $isLinkNode, $toggleLink, $createLinkNode, LinkNode, AutoLinkNode } from '@lexical/link';
|
|
7
7
|
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
8
|
-
import {
|
|
8
|
+
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
9
9
|
import { registerHistory, createEmptyHistoryState } from '@lexical/history';
|
|
10
10
|
import { DirectUpload } from '@rails/activestorage';
|
|
11
|
+
import { marked } from 'marked';
|
|
11
12
|
import 'prismjs/components/prism-ruby';
|
|
12
13
|
|
|
13
14
|
DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
@@ -16,6 +17,11 @@ DOMPurify.addHook("uponSanitizeElement", (node, data) => {
|
|
|
16
17
|
}
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
const getNonce = () => {
|
|
21
|
+
const element = document.head.querySelector("meta[name=csp-nonce]");
|
|
22
|
+
return element?.content
|
|
23
|
+
};
|
|
24
|
+
|
|
19
25
|
function getNearestListItemNode(node) {
|
|
20
26
|
let current = node;
|
|
21
27
|
while (current !== null) {
|
|
@@ -209,6 +215,7 @@ class LexicalToolbarElement extends HTMLElement {
|
|
|
209
215
|
this.#compactMenu();
|
|
210
216
|
|
|
211
217
|
this.#overflow.style.display = this.#overflowMenu.children.length ? "block" : "none";
|
|
218
|
+
this.#overflow.setAttribute("nonce", getNonce());
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
get #overflow() {
|
|
@@ -1399,6 +1406,7 @@ class Selection {
|
|
|
1399
1406
|
marker.style.width = "1px";
|
|
1400
1407
|
marker.style.height = "1em";
|
|
1401
1408
|
marker.style.lineHeight = "normal";
|
|
1409
|
+
marker.setAttribute("nonce", getNonce());
|
|
1402
1410
|
return marker
|
|
1403
1411
|
}
|
|
1404
1412
|
|
|
@@ -1512,6 +1520,105 @@ class Selection {
|
|
|
1512
1520
|
}
|
|
1513
1521
|
}
|
|
1514
1522
|
|
|
1523
|
+
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
1524
|
+
static getType() {
|
|
1525
|
+
return "custom_action_text_attachment"
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
static clone(node) {
|
|
1529
|
+
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
static importJSON(serializedNode) {
|
|
1533
|
+
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
static importDOM() {
|
|
1537
|
+
return {
|
|
1538
|
+
"action-text-attachment": (attachment) => {
|
|
1539
|
+
const content = attachment.getAttribute("content");
|
|
1540
|
+
if (!attachment.getAttribute("content")) {
|
|
1541
|
+
return null
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return {
|
|
1545
|
+
conversion: () => {
|
|
1546
|
+
// Preserve initial space if present since Lexical removes it
|
|
1547
|
+
const nodes = [];
|
|
1548
|
+
const previousSibling = attachment.previousSibling;
|
|
1549
|
+
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
1550
|
+
nodes.push($createTextNode(" "));
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
nodes.push(new CustomActionTextAttachmentNode({
|
|
1554
|
+
sgid: attachment.getAttribute("sgid"),
|
|
1555
|
+
innerHtml: JSON.parse(content),
|
|
1556
|
+
contentType: attachment.getAttribute("content-type")
|
|
1557
|
+
}));
|
|
1558
|
+
|
|
1559
|
+
nodes.push($createTextNode(" "));
|
|
1560
|
+
|
|
1561
|
+
return { node: nodes }
|
|
1562
|
+
},
|
|
1563
|
+
priority: 2
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
constructor({ sgid, contentType, innerHtml }, key) {
|
|
1570
|
+
super(key);
|
|
1571
|
+
|
|
1572
|
+
this.sgid = sgid;
|
|
1573
|
+
this.contentType = contentType || "application/vnd.actiontext.unknown";
|
|
1574
|
+
this.innerHtml = innerHtml;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
createDOM() {
|
|
1578
|
+
const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
1579
|
+
|
|
1580
|
+
figure.addEventListener("click", (event) => {
|
|
1581
|
+
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
1585
|
+
|
|
1586
|
+
return figure
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
updateDOM() {
|
|
1590
|
+
return true
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
isInline() {
|
|
1594
|
+
return true
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
exportDOM() {
|
|
1598
|
+
const attachment = createElement("action-text-attachment", {
|
|
1599
|
+
sgid: this.sgid,
|
|
1600
|
+
content: JSON.stringify(this.innerHtml),
|
|
1601
|
+
"content-type": this.contentType
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
return { element: attachment }
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
exportJSON() {
|
|
1608
|
+
return {
|
|
1609
|
+
type: "custom_action_text_attachment",
|
|
1610
|
+
version: 1,
|
|
1611
|
+
sgid: this.sgid,
|
|
1612
|
+
contentType: this.contentType,
|
|
1613
|
+
innerHtml: this.innerHtml
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
decorate() {
|
|
1618
|
+
return null
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1515
1622
|
class Contents {
|
|
1516
1623
|
constructor(editorElement) {
|
|
1517
1624
|
this.editorElement = editorElement;
|
|
@@ -1630,6 +1737,24 @@ class Contents {
|
|
|
1630
1737
|
});
|
|
1631
1738
|
}
|
|
1632
1739
|
|
|
1740
|
+
createLink(url) {
|
|
1741
|
+
let linkNodeKey = null;
|
|
1742
|
+
|
|
1743
|
+
this.editor.update(() => {
|
|
1744
|
+
const textNode = $createTextNode(url);
|
|
1745
|
+
const linkNode = $createLinkNode(url);
|
|
1746
|
+
linkNode.append(textNode);
|
|
1747
|
+
|
|
1748
|
+
const selection = $getSelection();
|
|
1749
|
+
if ($isRangeSelection(selection)) {
|
|
1750
|
+
selection.insertNodes([linkNode]);
|
|
1751
|
+
linkNodeKey = linkNode.getKey();
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
return linkNodeKey
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1633
1758
|
createLinkWithSelectedText(url) {
|
|
1634
1759
|
if (!this.hasSelectedText()) return
|
|
1635
1760
|
|
|
@@ -1758,6 +1883,47 @@ class Contents {
|
|
|
1758
1883
|
});
|
|
1759
1884
|
}
|
|
1760
1885
|
|
|
1886
|
+
replaceNodeWithHTML(nodeKey, html, options = {}) {
|
|
1887
|
+
this.editor.update(() => {
|
|
1888
|
+
const node = $getNodeByKey(nodeKey);
|
|
1889
|
+
if (!node) return
|
|
1890
|
+
|
|
1891
|
+
const selection = $getSelection();
|
|
1892
|
+
let wasSelected = false;
|
|
1893
|
+
|
|
1894
|
+
if ($isRangeSelection(selection)) {
|
|
1895
|
+
const selectedNodes = selection.getNodes();
|
|
1896
|
+
wasSelected = selectedNodes.includes(node) || selectedNodes.some(n => n.getParent() === node);
|
|
1897
|
+
|
|
1898
|
+
if (wasSelected) {
|
|
1899
|
+
$setSelection(null);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const replacementNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
1904
|
+
node.replace(replacementNode);
|
|
1905
|
+
|
|
1906
|
+
if (wasSelected) {
|
|
1907
|
+
replacementNode.selectEnd();
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
insertHTMLBelowNode(nodeKey, html, options = {}) {
|
|
1913
|
+
this.editor.update(() => {
|
|
1914
|
+
const node = $getNodeByKey(nodeKey);
|
|
1915
|
+
if (!node) return
|
|
1916
|
+
|
|
1917
|
+
let previousNode = node;
|
|
1918
|
+
try {
|
|
1919
|
+
previousNode = node.getTopLevelElementOrThrow();
|
|
1920
|
+
} catch {}
|
|
1921
|
+
|
|
1922
|
+
const newNode = options.attachment ? this.#createCustomAttachmentNodeWithHtml(html, options.attachment) : this.#createHtmlNodeWith(html);
|
|
1923
|
+
previousNode.insertAfter(newNode);
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1761
1927
|
get #selection() {
|
|
1762
1928
|
return this.editorElement.selection
|
|
1763
1929
|
}
|
|
@@ -2000,6 +2166,21 @@ class Contents {
|
|
|
2000
2166
|
}
|
|
2001
2167
|
}
|
|
2002
2168
|
|
|
2169
|
+
#createCustomAttachmentNodeWithHtml(html, options = {}) {
|
|
2170
|
+
const attachmentConfig = typeof options === 'object' ? options : {};
|
|
2171
|
+
|
|
2172
|
+
return new CustomActionTextAttachmentNode({
|
|
2173
|
+
sgid: attachmentConfig.sgid || null,
|
|
2174
|
+
contentType: "text/html",
|
|
2175
|
+
innerHtml: html
|
|
2176
|
+
})
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
#createHtmlNodeWith(html) {
|
|
2180
|
+
const htmlNodes = $generateNodesFromDOM(this.editor, parseHtml(html));
|
|
2181
|
+
return htmlNodes[0] || $createParagraphNode()
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2003
2184
|
#shouldUploadFile(file) {
|
|
2004
2185
|
return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
|
|
2005
2186
|
}
|
|
@@ -2055,18 +2236,32 @@ class Clipboard {
|
|
|
2055
2236
|
item.getAsString((text) => {
|
|
2056
2237
|
if (isUrl(text) && this.contents.hasSelectedText()) {
|
|
2057
2238
|
this.contents.createLinkWithSelectedText(text);
|
|
2239
|
+
} else if (isUrl(text)) {
|
|
2240
|
+
const nodeKey = this.contents.createLink(text);
|
|
2241
|
+
this.#dispatchLinkInsertEvent(nodeKey, { url: text });
|
|
2058
2242
|
} else {
|
|
2059
2243
|
this.#pasteMarkdown(text);
|
|
2060
2244
|
}
|
|
2061
2245
|
});
|
|
2062
2246
|
}
|
|
2063
2247
|
|
|
2064
|
-
#
|
|
2065
|
-
|
|
2066
|
-
|
|
2248
|
+
#dispatchLinkInsertEvent(nodeKey, payload) {
|
|
2249
|
+
const linkManipulationMethods = {
|
|
2250
|
+
replaceLinkWith: (html, options) => this.contents.replaceNodeWithHTML(nodeKey, html, options),
|
|
2251
|
+
insertBelowLink: (html, options) => this.contents.insertHTMLBelowNode(nodeKey, html, options)
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
dispatch(this.editorElement, "lexxy:insert-link", {
|
|
2255
|
+
...payload,
|
|
2256
|
+
...linkManipulationMethods
|
|
2067
2257
|
});
|
|
2068
2258
|
}
|
|
2069
2259
|
|
|
2260
|
+
#pasteMarkdown(text) {
|
|
2261
|
+
const html = marked(text);
|
|
2262
|
+
this.contents.insertHtml(html);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2070
2265
|
#handlePastedFiles(clipboardData) {
|
|
2071
2266
|
if (!this.editorElement.supportsAttachments) return
|
|
2072
2267
|
|
|
@@ -2094,105 +2289,6 @@ class Clipboard {
|
|
|
2094
2289
|
}
|
|
2095
2290
|
}
|
|
2096
2291
|
|
|
2097
|
-
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
2098
|
-
static getType() {
|
|
2099
|
-
return "custom_action_text_attachment"
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
static clone(node) {
|
|
2103
|
-
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
static importJSON(serializedNode) {
|
|
2107
|
-
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
static importDOM() {
|
|
2111
|
-
return {
|
|
2112
|
-
"action-text-attachment": (attachment) => {
|
|
2113
|
-
const content = attachment.getAttribute("content");
|
|
2114
|
-
if (!attachment.getAttribute("content")) {
|
|
2115
|
-
return null
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
return {
|
|
2119
|
-
conversion: () => {
|
|
2120
|
-
// Preserve initial space if present since Lexical removes it
|
|
2121
|
-
const nodes = [];
|
|
2122
|
-
const previousSibling = attachment.previousSibling;
|
|
2123
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
2124
|
-
nodes.push($createTextNode(" "));
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
nodes.push(new CustomActionTextAttachmentNode({
|
|
2128
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2129
|
-
innerHtml: JSON.parse(content),
|
|
2130
|
-
contentType: attachment.getAttribute("content-type")
|
|
2131
|
-
}));
|
|
2132
|
-
|
|
2133
|
-
nodes.push($createTextNode(" "));
|
|
2134
|
-
|
|
2135
|
-
return { node: nodes }
|
|
2136
|
-
},
|
|
2137
|
-
priority: 2
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
constructor({ sgid, contentType, innerHtml }, key) {
|
|
2144
|
-
super(key);
|
|
2145
|
-
|
|
2146
|
-
this.sgid = sgid;
|
|
2147
|
-
this.contentType = contentType || "application/vnd.actiontext.unknown";
|
|
2148
|
-
this.innerHtml = innerHtml;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
createDOM() {
|
|
2152
|
-
const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
2153
|
-
|
|
2154
|
-
figure.addEventListener("click", (event) => {
|
|
2155
|
-
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
2156
|
-
});
|
|
2157
|
-
|
|
2158
|
-
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
2159
|
-
|
|
2160
|
-
return figure
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
updateDOM() {
|
|
2164
|
-
return true
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
isInline() {
|
|
2168
|
-
return true
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
exportDOM() {
|
|
2172
|
-
const attachment = createElement("action-text-attachment", {
|
|
2173
|
-
sgid: this.sgid,
|
|
2174
|
-
content: JSON.stringify(this.innerHtml),
|
|
2175
|
-
"content-type": this.contentType
|
|
2176
|
-
});
|
|
2177
|
-
|
|
2178
|
-
return { element: attachment }
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
exportJSON() {
|
|
2182
|
-
return {
|
|
2183
|
-
type: "custom_action_text_attachment",
|
|
2184
|
-
version: 1,
|
|
2185
|
-
sgid: this.sgid,
|
|
2186
|
-
contentType: this.contentType,
|
|
2187
|
-
innerHtml: this.innerHtml
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
decorate() {
|
|
2192
|
-
return null
|
|
2193
|
-
}
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
2292
|
class LexicalEditorElement extends HTMLElement {
|
|
2197
2293
|
static formAssociated = true
|
|
2198
2294
|
static debug = true
|
|
@@ -2427,6 +2523,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2427
2523
|
this.cachedValue = null;
|
|
2428
2524
|
this.#internalFormValue = this.value;
|
|
2429
2525
|
this.#toggleEmptyStatus();
|
|
2526
|
+
this.#validateRequired();
|
|
2430
2527
|
}));
|
|
2431
2528
|
}
|
|
2432
2529
|
|
|
@@ -2533,6 +2630,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2533
2630
|
return !this.editorContentElement.textContent.trim() && !containsVisuallyRelevantChildren(this.editorContentElement)
|
|
2534
2631
|
}
|
|
2535
2632
|
|
|
2633
|
+
#validateRequired() {
|
|
2634
|
+
if (this.hasAttribute("required") && this.#isEmpty) {
|
|
2635
|
+
this.internals.setValidity({ valueMissing: true }, "Please fill out this field.", this.editorContentElement);
|
|
2636
|
+
} else {
|
|
2637
|
+
this.internals.setValidity({});
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2536
2641
|
#reset() {
|
|
2537
2642
|
this.#unregisterHandlers();
|
|
2538
2643
|
|
|
@@ -3065,6 +3170,7 @@ class LexicalPromptElement extends HTMLElement {
|
|
|
3065
3170
|
const popoverContainer = createElement("ul", { role: "listbox", id: generateDomId("prompt-popover") }); // Avoiding [popover] due to not being able to position at an arbitrary X, Y position.
|
|
3066
3171
|
popoverContainer.classList.add("lexxy-prompt-menu");
|
|
3067
3172
|
popoverContainer.style.position = "absolute";
|
|
3173
|
+
popoverContainer.setAttribute("nonce", getNonce());
|
|
3068
3174
|
popoverContainer.append(...(await this.source.buildListItems()));
|
|
3069
3175
|
popoverContainer.addEventListener("click", this.#handlePopoverClick);
|
|
3070
3176
|
this.#editorElement.appendChild(popoverContainer);
|
|
@@ -3099,6 +3205,7 @@ class CodeLanguagePicker extends HTMLElement {
|
|
|
3099
3205
|
});
|
|
3100
3206
|
|
|
3101
3207
|
this.languagePickerElement.style.position = "absolute";
|
|
3208
|
+
this.languagePickerElement.setAttribute("nonce", getNonce());
|
|
3102
3209
|
this.editorElement.appendChild(this.languagePickerElement);
|
|
3103
3210
|
}
|
|
3104
3211
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@37signals/lexxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10-beta",
|
|
4
4
|
"description": "Lexxy - A modern rich text editor for Rails.",
|
|
5
5
|
"module": "dist/lexxy.esm.js",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@lexical/table": "^0.32.1",
|
|
38
38
|
"@lexical/utils": "^0.32.1",
|
|
39
39
|
"dompurify": "^3.2.6",
|
|
40
|
+
"marked": "^16.3.0",
|
|
40
41
|
"prismjs": "^1.30.0"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|