@37signals/lexxy 0.1.10-beta → 0.1.12-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/dist/lexxy.esm.js +217 -22
- package/package.json +1 -1
package/dist/lexxy.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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,
|
|
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_RIGHT_COMMAND, KEY_ARROW_UP_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';
|
|
@@ -42,6 +42,17 @@ function getListType(node) {
|
|
|
42
42
|
return null
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function isPrintableCharacter(event) {
|
|
46
|
+
// Ignore if modifier keys are pressed (except Shift for uppercase)
|
|
47
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false
|
|
48
|
+
|
|
49
|
+
// Ignore special keys
|
|
50
|
+
if (event.key.length > 1 && event.key !== 'Enter' && event.key !== 'Space') return false
|
|
51
|
+
|
|
52
|
+
// Accept single character keys (letters, numbers, punctuation)
|
|
53
|
+
return event.key.length === 1
|
|
54
|
+
}
|
|
55
|
+
|
|
45
56
|
class LexicalToolbarElement extends HTMLElement {
|
|
46
57
|
constructor() {
|
|
47
58
|
super();
|
|
@@ -482,6 +493,7 @@ class ActionTextAttachmentNode extends DecoratorNode {
|
|
|
482
493
|
conversion: () => ({
|
|
483
494
|
node: new ActionTextAttachmentNode({
|
|
484
495
|
src: img.getAttribute("src"),
|
|
496
|
+
caption: img.getAttribute("alt") || "",
|
|
485
497
|
contentType: "image/*",
|
|
486
498
|
width: img.getAttribute("width"),
|
|
487
499
|
height: img.getAttribute("height")
|
|
@@ -881,6 +893,8 @@ class CommandDispatcher {
|
|
|
881
893
|
|
|
882
894
|
dispatchInsertUnorderedList() {
|
|
883
895
|
const selection = $getSelection();
|
|
896
|
+
if (!selection) return;
|
|
897
|
+
|
|
884
898
|
const anchorNode = selection.anchor.getNode();
|
|
885
899
|
|
|
886
900
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "bullet") {
|
|
@@ -892,6 +906,8 @@ class CommandDispatcher {
|
|
|
892
906
|
|
|
893
907
|
dispatchInsertOrderedList() {
|
|
894
908
|
const selection = $getSelection();
|
|
909
|
+
if (!selection) return;
|
|
910
|
+
|
|
895
911
|
const anchorNode = selection.anchor.getNode();
|
|
896
912
|
|
|
897
913
|
if (this.selection.isInsideList && anchorNode && getListType(anchorNode) === "number") {
|
|
@@ -1064,11 +1080,14 @@ function nextFrame() {
|
|
|
1064
1080
|
class Selection {
|
|
1065
1081
|
constructor(editorElement) {
|
|
1066
1082
|
this.editorElement = editorElement;
|
|
1083
|
+
this.editorContentElement = editorElement.editorContentElement;
|
|
1067
1084
|
this.editor = this.editorElement.editor;
|
|
1068
1085
|
this.previouslySelectedKeys = new Set();
|
|
1069
1086
|
|
|
1070
1087
|
this.#listenForNodeSelections();
|
|
1071
1088
|
this.#processSelectionChangeCommands();
|
|
1089
|
+
this.#handleInputWhenDecoratorNodesSelected();
|
|
1090
|
+
this.#containEditorFocus();
|
|
1072
1091
|
}
|
|
1073
1092
|
|
|
1074
1093
|
clear() {
|
|
@@ -1162,6 +1181,21 @@ class Selection {
|
|
|
1162
1181
|
return this.#findNextSiblingUp(anchorNode)
|
|
1163
1182
|
}
|
|
1164
1183
|
|
|
1184
|
+
get topLevelNodeAfterCursor() {
|
|
1185
|
+
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
1186
|
+
if (!anchorNode) return null
|
|
1187
|
+
|
|
1188
|
+
if ($isTextNode(anchorNode)) {
|
|
1189
|
+
return this.#getNextNodeFromTextEnd(anchorNode)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if ($isElementNode(anchorNode)) {
|
|
1193
|
+
return this.#getNodeAfterElementNode(anchorNode, offset)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
return this.#findNextSiblingUp(anchorNode)
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1165
1199
|
get nodeBeforeCursor() {
|
|
1166
1200
|
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
1167
1201
|
if (!anchorNode) return null
|
|
@@ -1177,6 +1211,21 @@ class Selection {
|
|
|
1177
1211
|
return this.#findPreviousSiblingUp(anchorNode)
|
|
1178
1212
|
}
|
|
1179
1213
|
|
|
1214
|
+
get topLevelNodeBeforeCursor() {
|
|
1215
|
+
const { anchorNode, offset } = this.#getCollapsedSelectionData();
|
|
1216
|
+
if (!anchorNode) return null
|
|
1217
|
+
|
|
1218
|
+
if ($isTextNode(anchorNode)) {
|
|
1219
|
+
return this.#getPreviousNodeFromTextStart(anchorNode)
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
if ($isElementNode(anchorNode)) {
|
|
1223
|
+
return this.#getNodeBeforeElementNode(anchorNode, offset)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
return this.#findPreviousSiblingUp(anchorNode)
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1180
1229
|
get #contents() {
|
|
1181
1230
|
return this.editorElement.contents
|
|
1182
1231
|
}
|
|
@@ -1197,9 +1246,9 @@ class Selection {
|
|
|
1197
1246
|
|
|
1198
1247
|
#processSelectionChangeCommands() {
|
|
1199
1248
|
this.editor.registerCommand(KEY_ARROW_LEFT_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
1200
|
-
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
1201
1249
|
this.editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, this.#selectNextNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
1202
|
-
this.editor.registerCommand(
|
|
1250
|
+
this.editor.registerCommand(KEY_ARROW_UP_COMMAND, this.#selectPreviousTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
1251
|
+
this.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, this.#selectNextTopLevelNode.bind(this), COMMAND_PRIORITY_LOW);
|
|
1203
1252
|
|
|
1204
1253
|
this.editor.registerCommand(KEY_DELETE_COMMAND, this.#deleteSelectedOrNext.bind(this), COMMAND_PRIORITY_LOW);
|
|
1205
1254
|
this.editor.registerCommand(KEY_BACKSPACE_COMMAND, this.#deletePreviousOrNext.bind(this), COMMAND_PRIORITY_LOW);
|
|
@@ -1230,6 +1279,93 @@ class Selection {
|
|
|
1230
1279
|
});
|
|
1231
1280
|
}
|
|
1232
1281
|
|
|
1282
|
+
// In Safari, when the only node in the document is an attachment, it won't let you enter text
|
|
1283
|
+
// before/below it. There is probably a better fix here, but this workaround solves the problem until
|
|
1284
|
+
// we find it.
|
|
1285
|
+
#handleInputWhenDecoratorNodesSelected() {
|
|
1286
|
+
this.editor.getRootElement().addEventListener("keydown", (event) => {
|
|
1287
|
+
if (isPrintableCharacter(event)) {
|
|
1288
|
+
this.editor.update(() => {
|
|
1289
|
+
const selection = $getSelection();
|
|
1290
|
+
|
|
1291
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
1292
|
+
const anchorNode = selection.anchor.getNode();
|
|
1293
|
+
const offset = selection.anchor.offset;
|
|
1294
|
+
|
|
1295
|
+
const nodeBefore = this.#getNodeBeforePosition(anchorNode, offset);
|
|
1296
|
+
const nodeAfter = this.#getNodeAfterPosition(anchorNode, offset);
|
|
1297
|
+
|
|
1298
|
+
if (nodeBefore instanceof DecoratorNode && !nodeBefore.isInline()) {
|
|
1299
|
+
event.preventDefault();
|
|
1300
|
+
this.#contents.createParagraphAfterNode(nodeBefore, event.key);
|
|
1301
|
+
return
|
|
1302
|
+
} else if (nodeAfter instanceof DecoratorNode && !nodeAfter.isInline()) {
|
|
1303
|
+
event.preventDefault();
|
|
1304
|
+
this.#contents.createParagraphBeforeNode(nodeAfter, event.key);
|
|
1305
|
+
return
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
}, true);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
#getNodeBeforePosition(node, offset) {
|
|
1314
|
+
if ($isTextNode(node) && offset === 0) {
|
|
1315
|
+
return node.getPreviousSibling()
|
|
1316
|
+
}
|
|
1317
|
+
if ($isElementNode(node) && offset > 0) {
|
|
1318
|
+
return node.getChildAtIndex(offset - 1)
|
|
1319
|
+
}
|
|
1320
|
+
return null
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
#getNodeAfterPosition(node, offset) {
|
|
1324
|
+
if ($isTextNode(node) && offset === node.getTextContentSize()) {
|
|
1325
|
+
return node.getNextSibling()
|
|
1326
|
+
}
|
|
1327
|
+
if ($isElementNode(node)) {
|
|
1328
|
+
return node.getChildAtIndex(offset)
|
|
1329
|
+
}
|
|
1330
|
+
return null
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
#containEditorFocus() {
|
|
1334
|
+
// Workaround for a bizarre Chrome bug where the cursor abandons the editor to focus on not-focusable elements
|
|
1335
|
+
// above when navigating UP/DOWN when Lexical shows its fake cursor on custom decorator nodes.
|
|
1336
|
+
this.editorContentElement.addEventListener("keydown", (event) => {
|
|
1337
|
+
if (event.key === "ArrowUp") {
|
|
1338
|
+
const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
|
|
1339
|
+
|
|
1340
|
+
if (lexicalCursor) {
|
|
1341
|
+
let currentElement = lexicalCursor.previousElementSibling;
|
|
1342
|
+
while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
|
|
1343
|
+
currentElement = currentElement.previousElementSibling;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
if (!currentElement) {
|
|
1347
|
+
event.preventDefault();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (event.key === "ArrowDown") {
|
|
1353
|
+
const lexicalCursor = this.editor.getRootElement().querySelector('[data-lexical-cursor]');
|
|
1354
|
+
|
|
1355
|
+
if (lexicalCursor) {
|
|
1356
|
+
let currentElement = lexicalCursor.nextElementSibling;
|
|
1357
|
+
while (currentElement && currentElement.hasAttribute('data-lexical-cursor')) {
|
|
1358
|
+
currentElement = currentElement.nextElementSibling;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!currentElement) {
|
|
1362
|
+
event.preventDefault();
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}, true);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1233
1369
|
#syncSelectedClasses() {
|
|
1234
1370
|
this.#clearPreviouslyHighlightedItems();
|
|
1235
1371
|
this.#highlightNewItems();
|
|
@@ -1272,6 +1408,22 @@ class Selection {
|
|
|
1272
1408
|
}
|
|
1273
1409
|
}
|
|
1274
1410
|
|
|
1411
|
+
async #selectPreviousTopLevelNode() {
|
|
1412
|
+
if (this.current) {
|
|
1413
|
+
await this.#withCurrentNode((currentNode) => currentNode.selectPrevious());
|
|
1414
|
+
} else {
|
|
1415
|
+
this.#selectInLexical(this.topLevelNodeBeforeCursor);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
async #selectNextTopLevelNode() {
|
|
1420
|
+
if (this.current) {
|
|
1421
|
+
await this.#withCurrentNode((currentNode) => currentNode.selectNext(0, 0));
|
|
1422
|
+
} else {
|
|
1423
|
+
this.#selectInLexical(this.topLevelNodeAfterCursor);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1275
1427
|
async #withCurrentNode(fn) {
|
|
1276
1428
|
await nextFrame();
|
|
1277
1429
|
if (this.current) {
|
|
@@ -1826,6 +1978,30 @@ class Contents {
|
|
|
1826
1978
|
});
|
|
1827
1979
|
}
|
|
1828
1980
|
|
|
1981
|
+
createParagraphAfterNode(node, text) {
|
|
1982
|
+
const newParagraph = $createParagraphNode();
|
|
1983
|
+
node.insertAfter(newParagraph);
|
|
1984
|
+
newParagraph.selectStart();
|
|
1985
|
+
|
|
1986
|
+
// Insert the typed text
|
|
1987
|
+
if (text) {
|
|
1988
|
+
newParagraph.append($createTextNode(text));
|
|
1989
|
+
newParagraph.select(1, 1); // Place cursor after the text
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
createParagraphBeforeNode(node, text) {
|
|
1994
|
+
const newParagraph = $createParagraphNode();
|
|
1995
|
+
node.insertBefore(newParagraph);
|
|
1996
|
+
newParagraph.selectStart();
|
|
1997
|
+
|
|
1998
|
+
// Insert the typed text
|
|
1999
|
+
if (text) {
|
|
2000
|
+
newParagraph.append($createTextNode(text));
|
|
2001
|
+
newParagraph.select(1, 1); // Place cursor after the text
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1829
2005
|
uploadFile(file) {
|
|
1830
2006
|
if (!this.editorElement.supportsAttachments) {
|
|
1831
2007
|
console.warn("This editor does not supports attachments (it's configured with [attachments=false])");
|
|
@@ -1860,25 +2036,34 @@ class Contents {
|
|
|
1860
2036
|
deleteSelectedNodes() {
|
|
1861
2037
|
this.editor.update(() => {
|
|
1862
2038
|
if ($isNodeSelection(this.#selection.current)) {
|
|
1863
|
-
|
|
1864
|
-
|
|
2039
|
+
const nodesToRemove = this.#selection.current.getNodes();
|
|
2040
|
+
if (nodesToRemove.length === 0) return
|
|
2041
|
+
|
|
2042
|
+
// Use splice() instead of node.remove() for proper removal and
|
|
2043
|
+
// reconciliation. Would have issues with removing unintended decorator nodes
|
|
2044
|
+
// with node.remove()
|
|
2045
|
+
nodesToRemove.forEach((node) => {
|
|
1865
2046
|
const parent = node.getParent();
|
|
2047
|
+
if (!$isElementNode(parent)) return
|
|
1866
2048
|
|
|
1867
|
-
|
|
2049
|
+
const children = parent.getChildren();
|
|
2050
|
+
const index = children.indexOf(node);
|
|
1868
2051
|
|
|
1869
|
-
if (
|
|
1870
|
-
parent.
|
|
2052
|
+
if (index >= 0) {
|
|
2053
|
+
parent.splice(index, 1, []);
|
|
1871
2054
|
}
|
|
1872
|
-
|
|
1873
|
-
nodesWereRemoved = true;
|
|
1874
2055
|
});
|
|
1875
2056
|
|
|
1876
|
-
if
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
return true
|
|
2057
|
+
// Check if root is empty after all removals
|
|
2058
|
+
const root = $getRoot();
|
|
2059
|
+
if (root.getChildrenSize() === 0) {
|
|
2060
|
+
root.append($createParagraphNode());
|
|
1881
2061
|
}
|
|
2062
|
+
|
|
2063
|
+
this.#selection.clear();
|
|
2064
|
+
this.editor.focus();
|
|
2065
|
+
|
|
2066
|
+
return true
|
|
1882
2067
|
}
|
|
1883
2068
|
});
|
|
1884
2069
|
}
|
|
@@ -2265,6 +2450,9 @@ class Clipboard {
|
|
|
2265
2450
|
#handlePastedFiles(clipboardData) {
|
|
2266
2451
|
if (!this.editorElement.supportsAttachments) return
|
|
2267
2452
|
|
|
2453
|
+
const html = clipboardData.getData('text/html');
|
|
2454
|
+
if (html) return // Ignore if image copied from browser since we will load it as a remote image
|
|
2455
|
+
|
|
2268
2456
|
this.#preservingScrollPosition(() => {
|
|
2269
2457
|
for (const item of clipboardData.items) {
|
|
2270
2458
|
const file = item.getAsFile();
|
|
@@ -2294,9 +2482,10 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2294
2482
|
static debug = true
|
|
2295
2483
|
static commands = [ "bold", "italic", "" ]
|
|
2296
2484
|
|
|
2297
|
-
static observedAttributes = [ "connected" ]
|
|
2485
|
+
static observedAttributes = [ "connected", "required" ]
|
|
2298
2486
|
|
|
2299
2487
|
#initialValue = ""
|
|
2488
|
+
#validationTextArea = document.createElement("textarea")
|
|
2300
2489
|
|
|
2301
2490
|
constructor() {
|
|
2302
2491
|
super();
|
|
@@ -2329,6 +2518,11 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2329
2518
|
if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
|
|
2330
2519
|
requestAnimationFrame(() => this.#reconnect());
|
|
2331
2520
|
}
|
|
2521
|
+
|
|
2522
|
+
if (name === "required" && this.isConnected) {
|
|
2523
|
+
this.#validationTextArea.required = this.hasAttribute("required");
|
|
2524
|
+
this.#setValidity();
|
|
2525
|
+
}
|
|
2332
2526
|
}
|
|
2333
2527
|
|
|
2334
2528
|
formResetCallback() {
|
|
@@ -2382,7 +2576,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2382
2576
|
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
|
|
2383
2577
|
const root = $getRoot();
|
|
2384
2578
|
root.clear();
|
|
2385
|
-
root.append(...this.#parseHtmlIntoLexicalNodes(html));
|
|
2579
|
+
if (html !== "") root.append(...this.#parseHtmlIntoLexicalNodes(html));
|
|
2386
2580
|
root.select();
|
|
2387
2581
|
|
|
2388
2582
|
this.#toggleEmptyStatus();
|
|
@@ -2495,6 +2689,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2495
2689
|
|
|
2496
2690
|
this.internals.setFormValue(html);
|
|
2497
2691
|
this._internalFormValue = html;
|
|
2692
|
+
this.#validationTextArea.value = this.#isEmpty ? "" : html;
|
|
2498
2693
|
|
|
2499
2694
|
if (changed) {
|
|
2500
2695
|
dispatch(this, "lexxy:change");
|
|
@@ -2523,7 +2718,7 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2523
2718
|
this.cachedValue = null;
|
|
2524
2719
|
this.#internalFormValue = this.value;
|
|
2525
2720
|
this.#toggleEmptyStatus();
|
|
2526
|
-
this.#
|
|
2721
|
+
this.#setValidity();
|
|
2527
2722
|
}));
|
|
2528
2723
|
}
|
|
2529
2724
|
|
|
@@ -2630,11 +2825,11 @@ class LexicalEditorElement extends HTMLElement {
|
|
|
2630
2825
|
return !this.editorContentElement.textContent.trim() && !containsVisuallyRelevantChildren(this.editorContentElement)
|
|
2631
2826
|
}
|
|
2632
2827
|
|
|
2633
|
-
#
|
|
2634
|
-
if (this.
|
|
2635
|
-
this.internals.setValidity({ valueMissing: true }, "Please fill out this field.", this.editorContentElement);
|
|
2636
|
-
} else {
|
|
2828
|
+
#setValidity() {
|
|
2829
|
+
if (this.#validationTextArea.validity.valid) {
|
|
2637
2830
|
this.internals.setValidity({});
|
|
2831
|
+
} else {
|
|
2832
|
+
this.internals.setValidity(this.#validationTextArea.validity, this.#validationTextArea.validationMessage, this.editorContentElement);
|
|
2638
2833
|
}
|
|
2639
2834
|
}
|
|
2640
2835
|
|