@37signals/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.
- package/README.md +78 -3
- package/dist/lexxy.esm.js +199 -101
- package/package.json +1 -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,9 +1,9 @@
|
|
|
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
8
|
import { registerMarkdownShortcuts, TRANSFORMERS } from '@lexical/markdown';
|
|
9
9
|
import { registerHistory, createEmptyHistoryState } from '@lexical/history';
|
|
@@ -1520,6 +1520,105 @@ class Selection {
|
|
|
1520
1520
|
}
|
|
1521
1521
|
}
|
|
1522
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
|
+
|
|
1523
1622
|
class Contents {
|
|
1524
1623
|
constructor(editorElement) {
|
|
1525
1624
|
this.editorElement = editorElement;
|
|
@@ -1638,6 +1737,24 @@ class Contents {
|
|
|
1638
1737
|
});
|
|
1639
1738
|
}
|
|
1640
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
|
+
|
|
1641
1758
|
createLinkWithSelectedText(url) {
|
|
1642
1759
|
if (!this.hasSelectedText()) return
|
|
1643
1760
|
|
|
@@ -1766,6 +1883,47 @@ class Contents {
|
|
|
1766
1883
|
});
|
|
1767
1884
|
}
|
|
1768
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
|
+
|
|
1769
1927
|
get #selection() {
|
|
1770
1928
|
return this.editorElement.selection
|
|
1771
1929
|
}
|
|
@@ -2008,6 +2166,21 @@ class Contents {
|
|
|
2008
2166
|
}
|
|
2009
2167
|
}
|
|
2010
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
|
+
|
|
2011
2184
|
#shouldUploadFile(file) {
|
|
2012
2185
|
return dispatch(this.editorElement, 'lexxy:file-accept', { file }, true)
|
|
2013
2186
|
}
|
|
@@ -2063,12 +2236,27 @@ class Clipboard {
|
|
|
2063
2236
|
item.getAsString((text) => {
|
|
2064
2237
|
if (isUrl(text) && this.contents.hasSelectedText()) {
|
|
2065
2238
|
this.contents.createLinkWithSelectedText(text);
|
|
2239
|
+
} else if (isUrl(text)) {
|
|
2240
|
+
const nodeKey = this.contents.createLink(text);
|
|
2241
|
+
this.#dispatchLinkInsertEvent(nodeKey, { url: text });
|
|
2066
2242
|
} else {
|
|
2067
2243
|
this.#pasteMarkdown(text);
|
|
2068
2244
|
}
|
|
2069
2245
|
});
|
|
2070
2246
|
}
|
|
2071
2247
|
|
|
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
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2072
2260
|
#pasteMarkdown(text) {
|
|
2073
2261
|
const html = marked(text);
|
|
2074
2262
|
this.contents.insertHtml(html);
|
|
@@ -2101,105 +2289,6 @@ class Clipboard {
|
|
|
2101
2289
|
}
|
|
2102
2290
|
}
|
|
2103
2291
|
|
|
2104
|
-
class CustomActionTextAttachmentNode extends DecoratorNode {
|
|
2105
|
-
static getType() {
|
|
2106
|
-
return "custom_action_text_attachment"
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
static clone(node) {
|
|
2110
|
-
return new CustomActionTextAttachmentNode({ ...node }, node.__key)
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
static importJSON(serializedNode) {
|
|
2114
|
-
return new CustomActionTextAttachmentNode({ ...serializedNode })
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
static importDOM() {
|
|
2118
|
-
return {
|
|
2119
|
-
"action-text-attachment": (attachment) => {
|
|
2120
|
-
const content = attachment.getAttribute("content");
|
|
2121
|
-
if (!attachment.getAttribute("content")) {
|
|
2122
|
-
return null
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
return {
|
|
2126
|
-
conversion: () => {
|
|
2127
|
-
// Preserve initial space if present since Lexical removes it
|
|
2128
|
-
const nodes = [];
|
|
2129
|
-
const previousSibling = attachment.previousSibling;
|
|
2130
|
-
if (previousSibling && previousSibling.nodeType === Node.TEXT_NODE && /\s$/.test(previousSibling.textContent)) {
|
|
2131
|
-
nodes.push($createTextNode(" "));
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
nodes.push(new CustomActionTextAttachmentNode({
|
|
2135
|
-
sgid: attachment.getAttribute("sgid"),
|
|
2136
|
-
innerHtml: JSON.parse(content),
|
|
2137
|
-
contentType: attachment.getAttribute("content-type")
|
|
2138
|
-
}));
|
|
2139
|
-
|
|
2140
|
-
nodes.push($createTextNode(" "));
|
|
2141
|
-
|
|
2142
|
-
return { node: nodes }
|
|
2143
|
-
},
|
|
2144
|
-
priority: 2
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
constructor({ sgid, contentType, innerHtml }, key) {
|
|
2151
|
-
super(key);
|
|
2152
|
-
|
|
2153
|
-
this.sgid = sgid;
|
|
2154
|
-
this.contentType = contentType || "application/vnd.actiontext.unknown";
|
|
2155
|
-
this.innerHtml = innerHtml;
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
createDOM() {
|
|
2159
|
-
const figure = createElement("action-text-attachment", { "content-type": this.contentType, "data-lexxy-decorator": true });
|
|
2160
|
-
|
|
2161
|
-
figure.addEventListener("click", (event) => {
|
|
2162
|
-
dispatchCustomEvent(figure, "lexxy:internal:select-node", { key: this.getKey() });
|
|
2163
|
-
});
|
|
2164
|
-
|
|
2165
|
-
figure.insertAdjacentHTML("beforeend", this.innerHtml);
|
|
2166
|
-
|
|
2167
|
-
return figure
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
updateDOM() {
|
|
2171
|
-
return true
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
isInline() {
|
|
2175
|
-
return true
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
exportDOM() {
|
|
2179
|
-
const attachment = createElement("action-text-attachment", {
|
|
2180
|
-
sgid: this.sgid,
|
|
2181
|
-
content: JSON.stringify(this.innerHtml),
|
|
2182
|
-
"content-type": this.contentType
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
return { element: attachment }
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
exportJSON() {
|
|
2189
|
-
return {
|
|
2190
|
-
type: "custom_action_text_attachment",
|
|
2191
|
-
version: 1,
|
|
2192
|
-
sgid: this.sgid,
|
|
2193
|
-
contentType: this.contentType,
|
|
2194
|
-
innerHtml: this.innerHtml
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
decorate() {
|
|
2199
|
-
return null
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
2292
|
class LexicalEditorElement extends HTMLElement {
|
|
2204
2293
|
static formAssociated = true
|
|
2205
2294
|
static debug = true
|
|
@@ -2434,6 +2523,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2434
2523
|
this.cachedValue = null;
|
|
2435
2524
|
this.#internalFormValue = this.value;
|
|
2436
2525
|
this.#toggleEmptyStatus();
|
|
2526
|
+
this.#validateRequired();
|
|
2437
2527
|
}));
|
|
2438
2528
|
}
|
|
2439
2529
|
|
|
@@ -2540,6 +2630,14 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2540
2630
|
return !this.editorContentElement.textContent.trim() && !containsVisuallyRelevantChildren(this.editorContentElement)
|
|
2541
2631
|
}
|
|
2542
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
|
+
|
|
2543
2641
|
#reset() {
|
|
2544
2642
|
this.#unregisterHandlers();
|
|
2545
2643
|
|