21 files changed

+292
-94
lines changed
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@
248248
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
249249
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
250250
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
251-
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
251+
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
252252
'pages_edit_set_changelog' => 'Set Changelog',
253253
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
254254
'pages_edit_enter_changelog' => 'Enter Changelog',
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import invariant from 'lexical/shared/invariant';
1717
import {
1818
$createLineBreakNode,
1919
$createParagraphNode,
20-
$createTextNode,
20+
$createTextNode, $getNearestNodeFromDOMNode,
2121
$isDecoratorNode,
2222
$isElementNode,
2323
$isLineBreakNode,
@@ -63,6 +63,7 @@ import {
6363
toggleTextFormatType,
6464
} from './LexicalUtils';
6565
import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
66+
import {$selectSingleNode} from "../../utils/selection";
6667

6768
export type TextPointType = {
6869
_selection: BaseSelection;
@@ -2568,6 +2569,17 @@ export function updateDOMSelection(
25682569
}
25692570

25702571
if (!$isRangeSelection(nextSelection)) {
2572+
2573+
// If the DOM selection enters a decorator node update the selection to a single node selection
2574+
if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) {
2575+
const node = $getNearestNodeFromDOMNode(focusDOMNode);
2576+
if ($isDecoratorNode(node)) {
2577+
domSelection.removeAllRanges();
2578+
$selectSingleNode(node);
2579+
return;
2580+
}
2581+
}
2582+
25712583
// We don't remove selection if the prevSelection is null because
25722584
// of editor.setRootElement(). If this occurs on init when the
25732585
// editor is already focused, then this can cause the editor to
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {sizeToPixels} from "../../../utils/dom";
22
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
3+
import {elem} from "../../../../services/dom";
34

45
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
56
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
@@ -82,6 +83,38 @@ export function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: Co
8283
nodeA.__dir !== nodeB.__dir;
8384
}
8485

86+
export function applyCommonPropertyChanges(prevNode: CommonBlockInterface, currentNode: CommonBlockInterface, element: HTMLElement): void {
87+
if (prevNode.__id !== currentNode.__id) {
88+
element.setAttribute('id', currentNode.__id);
89+
}
90+
91+
if (prevNode.__alignment !== currentNode.__alignment) {
92+
for (const alignment of validAlignments) {
93+
element.classList.remove('align-' + alignment);
94+
}
95+
96+
if (currentNode.__alignment) {
97+
element.classList.add('align-' + currentNode.__alignment);
98+
}
99+
}
100+
101+
if (prevNode.__inset !== currentNode.__inset) {
102+
if (currentNode.__inset) {
103+
element.style.paddingLeft = `${currentNode.__inset}px`;
104+
} else {
105+
element.style.removeProperty('paddingLeft');
106+
}
107+
}
108+
109+
if (prevNode.__dir !== currentNode.__dir) {
110+
if (currentNode.__dir) {
111+
element.dir = currentNode.__dir;
112+
} else {
113+
element.removeAttribute('dir');
114+
}
115+
}
116+
}
117+
85118
export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
86119
if (node.__id) {
87120
element.setAttribute('id', node.__id);
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
3030
import {getTable} from './LexicalTableSelectionHelpers';
3131
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
3232
import {
33+
applyCommonPropertyChanges,
3334
commonPropertiesDifferent, deserializeCommonBlockNode,
3435
setCommonBlockPropsFromElement,
3536
updateElementWithCommonBlockProps
3637
} from "lexical/nodes/common";
3738
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
38-
import {getTableColumnWidths} from "../../utils/tables";
39+
import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables";
3940

4041
export type SerializedTableNode = Spread<{
4142
colWidths: string[];
@@ -54,7 +55,7 @@ export class TableNode extends CommonBlockNode {
5455
static clone(node: TableNode): TableNode {
5556
const newNode = new TableNode(node.__key);
5657
copyCommonBlockProperties(node, newNode);
57-
newNode.__colWidths = node.__colWidths;
58+
newNode.__colWidths = [...node.__colWidths];
5859
newNode.__styles = new Map(node.__styles);
5960
return newNode;
6061
}
@@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode {
9899
updateElementWithCommonBlockProps(tableElement, this);
99100

100101
const colWidths = this.getColWidths();
101-
if (colWidths.length > 0) {
102-
const colgroup = el('colgroup');
103-
for (const width of colWidths) {
104-
const col = el('col');
105-
if (width) {
106-
col.style.width = width;
107-
}
108-
colgroup.append(col);
109-
}
102+
const colgroup = buildColgroupFromTableWidths(colWidths);
103+
if (colgroup) {
110104
tableElement.append(colgroup);
111105
}
112106

@@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode {
117111
return tableElement;
118112
}
119113

120-
updateDOM(_prevNode: TableNode): boolean {
121-
return commonPropertiesDifferent(_prevNode, this)
122-
|| this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
123-
|| this.__styles.size !== _prevNode.__styles.size
124-
|| (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
114+
updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean {
115+
applyCommonPropertyChanges(_prevNode, this, dom);
116+
117+
if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) {
118+
const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP');
119+
const newColGroup = buildColgroupFromTableWidths(this.__colWidths);
120+
if (existingColGroup) {
121+
existingColGroup.remove();
122+
}
123+
124+
if (newColGroup) {
125+
dom.prepend(newColGroup);
126+
}
127+
}
128+
129+
if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) {
130+
dom.style.cssText = '';
131+
for (const [name, value] of this.__styles.entries()) {
132+
dom.style.setProperty(name, value);
133+
}
134+
}
135+
136+
return false;
125137
}
126138

127139
exportDOM(editor: LexicalEditor): DOMExportOutput {
@@ -169,7 +181,7 @@ export class TableNode extends CommonBlockNode {
169181

170182
getColWidths(): string[] {
171183
const self = this.getLatest();
172-
return self.__colWidths;
184+
return [...self.__colWidths];
173185
}
174186

175187
getStyles(): StyleMap {
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {TableDOMTable, TableObserver} from './LexicalTableObserver';
7171
import {$isTableRowNode} from './LexicalTableRowNode';
7272
import {$isTableSelection} from './LexicalTableSelection';
7373
import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils';
74+
import {$selectOrCreateAdjacent} from "../../utils/nodes";
7475

7576
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
7677

@@ -915,9 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable {
915916
domRows.length = 0;
916917

917918
while (currentNode != null) {
918-
const nodeMame = currentNode.nodeName;
919+
const nodeName = currentNode.nodeName;
919920

920-
if (nodeMame === 'TD' || nodeMame === 'TH') {
921+
if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') {
922+
currentNode = currentNode.nextSibling;
923+
continue;
924+
}
925+
926+
if (nodeName === 'TD' || nodeName === 'TH') {
921927
const elem = currentNode as HTMLElement;
922928
const cell = {
923929
elem,
@@ -1108,7 +1114,7 @@ const selectTableNodeInDirection = (
11081114
false,
11091115
);
11101116
} else {
1111-
tableNode.selectPrevious();
1117+
$selectOrCreateAdjacent(tableNode, false);
11121118
}
11131119

11141120
return true;
@@ -1120,7 +1126,7 @@ const selectTableNodeInDirection = (
11201126
true,
11211127
);
11221128
} else {
1123-
tableNode.selectNext();
1129+
$selectOrCreateAdjacent(tableNode, true);
11241130
}
11251131

11261132
return true;
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
TableRowNode,
3636
} from './LexicalTableRowNode';
3737
import {$isTableSelection} from './LexicalTableSelection';
38+
import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
3839

3940
export function $createTableNodeWithDimensions(
4041
rowCount: number,
@@ -779,7 +780,7 @@ export function $computeTableMapSkipCellCheck(
779780
return tableMap[row] === undefined || tableMap[row][column] === undefined;
780781
}
781782

782-
const gridChildren = grid.getChildren();
783+
const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node));
783784
for (let i = 0; i < gridChildren.length; i++) {
784785
const row = gridChildren[i];
785786
invariant(
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
9595
return handled;
9696
}
9797

98+
function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): boolean {
99+
const regex = /https?:\/\/([^?#]*?)\.(png|jpeg|jpg|gif|webp|bmp|avif)/i
100+
const text = data.getData('text/plain');
101+
if (text && regex.test(text)) {
102+
context.editor.update(() => {
103+
const image = $createImageNode(text);
104+
$insertNodes([image]);
105+
image.select();
106+
});
107+
return true;
108+
}
109+
110+
return false;
111+
}
112+
98113
function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
99114
const editor = context.editor;
100115
return (event: DragEvent): boolean => {
@@ -138,7 +153,10 @@ function createPasteListener(context: EditorUiContext): (event: ClipboardEvent)
138153
return false;
139154
}
140155

141-
const handled = handleMediaInsert(event.clipboardData, context);
156+
const handled =
157+
handleImageLinkInsert(event.clipboardData, context) ||
158+
handleMediaInsert(event.clipboardData, context);
159+
142160
if (handled) {
143161
event.preventDefault();
144162
}
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ import {
1313
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
1414
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
1515
import {getLastSelection} from "../utils/selection";
16-
import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes";
16+
import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes";
1717
import {$setInsetForSelection} from "../utils/lists";
1818
import {$isListItemNode} from "@lexical/list";
1919
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
20+
import {$isDiagramNode} from "../utils/diagrams";
2021

2122
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
2223
if (nodes.length === 1) {
2324
const node = nodes[0];
24-
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
25+
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) {
2526
return true;
2627
}
2728
}
@@ -46,16 +47,21 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
4647
* Insert a new empty node before/after the selection if the selection contains a single
4748
* selected node (like image, media etc...).
4849
*/
49-
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
50+
function insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
5051
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
5152
if (isSingleSelectedNode(selectionNodes)) {
5253
const node = selectionNodes[0];
5354
const nearestBlock = $getNearestNodeBlockParent(node) || node;
55+
const insertBefore = event?.shiftKey === true;
5456
if (nearestBlock) {
5557
requestAnimationFrame(() => {
5658
editor.update(() => {
5759
const newParagraph = $createParagraphNode();
58-
nearestBlock.insertAfter(newParagraph);
60+
if (insertBefore) {
61+
nearestBlock.insertBefore(newParagraph);
62+
} else {
63+
nearestBlock.insertAfter(newParagraph);
64+
}
5965
newParagraph.select();
6066
});
6167
});
@@ -74,22 +80,9 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event:
7480
}
7581

7682
event?.preventDefault();
77-
7883
const node = selectionNodes[0];
79-
const nearestBlock = $getNearestNodeBlockParent(node) || node;
80-
let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
81-
8284
editor.update(() => {
83-
if (!target) {
84-
target = $createParagraphNode();
85-
if (after) {
86-
nearestBlock.insertAfter(target)
87-
} else {
88-
nearestBlock.insertBefore(target);
89-
}
90-
}
91-
92-
target.selectStart();
85+
$selectOrCreateAdjacent(node, after);
9386
});
9487

9588
return true;
@@ -219,7 +212,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
219212
}, COMMAND_PRIORITY_LOW);
220213

221214
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
222-
return insertAfterSingleSelectedNode(context.editor, event)
215+
return insertAdjacentToSingleSelectedNode(context.editor, event)
223216
|| moveAfterDetailsOnEmptyLine(context.editor, event);
224217
}, COMMAND_PRIORITY_LOW);
225218

Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {EditorDecorator} from "../framework/decorator";
22
import {EditorUiContext} from "../framework/core";
3-
import {BaseSelection} from "lexical";
3+
import {BaseSelection, CLICK_COMMAND, COMMAND_PRIORITY_NORMAL} from "lexical";
44
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
55
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
66
import {$openDrawingEditorForNode} from "../../utils/diagrams";
@@ -12,11 +12,17 @@ export class DiagramDecorator extends EditorDecorator {
1212
setup(context: EditorUiContext, element: HTMLElement) {
1313
const diagramNode = this.getNode();
1414
element.classList.add('editor-diagram');
15-
element.addEventListener('click', event => {
15+
16+
context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => {
17+
if (!element.contains(event.target as HTMLElement)) {
18+
return false;
19+
}
20+
1621
context.editor.update(() => {
1722
$selectSingleNode(this.getNode());
18-
})
19-
});
23+
});
24+
return true;
25+
}, COMMAND_PRIORITY_NORMAL);
2026

2127
element.addEventListener('dblclick', event => {
2228
context.editor.getEditorState().read(() => {

0 commit comments

Comments
 (0)