Merge pull request #5627 from BookStackApp/lexical_20250525
Lexical Editor: Further fixes
This commit is contained in:
commit
68df43e5a8
|
@ -248,7 +248,7 @@ return [
|
||||||
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
|
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
|
||||||
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
|
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
|
||||||
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
|
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
|
||||||
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
|
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
|
||||||
'pages_edit_set_changelog' => 'Set Changelog',
|
'pages_edit_set_changelog' => 'Set Changelog',
|
||||||
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
|
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
|
||||||
'pages_edit_enter_changelog' => 'Enter Changelog',
|
'pages_edit_enter_changelog' => 'Enter Changelog',
|
||||||
|
|
|
@ -17,7 +17,7 @@ import invariant from 'lexical/shared/invariant';
|
||||||
import {
|
import {
|
||||||
$createLineBreakNode,
|
$createLineBreakNode,
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$createTextNode,
|
$createTextNode, $getNearestNodeFromDOMNode,
|
||||||
$isDecoratorNode,
|
$isDecoratorNode,
|
||||||
$isElementNode,
|
$isElementNode,
|
||||||
$isLineBreakNode,
|
$isLineBreakNode,
|
||||||
|
@ -63,6 +63,7 @@ import {
|
||||||
toggleTextFormatType,
|
toggleTextFormatType,
|
||||||
} from './LexicalUtils';
|
} from './LexicalUtils';
|
||||||
import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
|
import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
|
||||||
|
import {$selectSingleNode} from "../../utils/selection";
|
||||||
|
|
||||||
export type TextPointType = {
|
export type TextPointType = {
|
||||||
_selection: BaseSelection;
|
_selection: BaseSelection;
|
||||||
|
@ -2568,6 +2569,17 @@ export function updateDOMSelection(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isRangeSelection(nextSelection)) {
|
if (!$isRangeSelection(nextSelection)) {
|
||||||
|
|
||||||
|
// If the DOM selection enters a decorator node update the selection to a single node selection
|
||||||
|
if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) {
|
||||||
|
const node = $getNearestNodeFromDOMNode(focusDOMNode);
|
||||||
|
if ($isDecoratorNode(node)) {
|
||||||
|
domSelection.removeAllRanges();
|
||||||
|
$selectSingleNode(node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We don't remove selection if the prevSelection is null because
|
// We don't remove selection if the prevSelection is null because
|
||||||
// of editor.setRootElement(). If this occurs on init when the
|
// of editor.setRootElement(). If this occurs on init when the
|
||||||
// editor is already focused, then this can cause the editor to
|
// editor is already focused, then this can cause the editor to
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {sizeToPixels} from "../../../utils/dom";
|
import {sizeToPixels} from "../../../utils/dom";
|
||||||
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
|
import {elem} from "../../../../services/dom";
|
||||||
|
|
||||||
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
|
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
|
||||||
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
|
const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify'];
|
||||||
|
@ -82,6 +83,38 @@ export function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: Co
|
||||||
nodeA.__dir !== nodeB.__dir;
|
nodeA.__dir !== nodeB.__dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyCommonPropertyChanges(prevNode: CommonBlockInterface, currentNode: CommonBlockInterface, element: HTMLElement): void {
|
||||||
|
if (prevNode.__id !== currentNode.__id) {
|
||||||
|
element.setAttribute('id', currentNode.__id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNode.__alignment !== currentNode.__alignment) {
|
||||||
|
for (const alignment of validAlignments) {
|
||||||
|
element.classList.remove('align-' + alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNode.__alignment) {
|
||||||
|
element.classList.add('align-' + currentNode.__alignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNode.__inset !== currentNode.__inset) {
|
||||||
|
if (currentNode.__inset) {
|
||||||
|
element.style.paddingLeft = `${currentNode.__inset}px`;
|
||||||
|
} else {
|
||||||
|
element.style.removeProperty('paddingLeft');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNode.__dir !== currentNode.__dir) {
|
||||||
|
if (currentNode.__dir) {
|
||||||
|
element.dir = currentNode.__dir;
|
||||||
|
} else {
|
||||||
|
element.removeAttribute('dir');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
|
export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
|
||||||
if (node.__id) {
|
if (node.__id) {
|
||||||
element.setAttribute('id', node.__id);
|
element.setAttribute('id', node.__id);
|
||||||
|
|
|
@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
|
||||||
import {getTable} from './LexicalTableSelectionHelpers';
|
import {getTable} from './LexicalTableSelectionHelpers';
|
||||||
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
|
||||||
import {
|
import {
|
||||||
|
applyCommonPropertyChanges,
|
||||||
commonPropertiesDifferent, deserializeCommonBlockNode,
|
commonPropertiesDifferent, deserializeCommonBlockNode,
|
||||||
setCommonBlockPropsFromElement,
|
setCommonBlockPropsFromElement,
|
||||||
updateElementWithCommonBlockProps
|
updateElementWithCommonBlockProps
|
||||||
} from "lexical/nodes/common";
|
} from "lexical/nodes/common";
|
||||||
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
|
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
|
||||||
import {getTableColumnWidths} from "../../utils/tables";
|
import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables";
|
||||||
|
|
||||||
export type SerializedTableNode = Spread<{
|
export type SerializedTableNode = Spread<{
|
||||||
colWidths: string[];
|
colWidths: string[];
|
||||||
|
@ -54,7 +55,7 @@ export class TableNode extends CommonBlockNode {
|
||||||
static clone(node: TableNode): TableNode {
|
static clone(node: TableNode): TableNode {
|
||||||
const newNode = new TableNode(node.__key);
|
const newNode = new TableNode(node.__key);
|
||||||
copyCommonBlockProperties(node, newNode);
|
copyCommonBlockProperties(node, newNode);
|
||||||
newNode.__colWidths = node.__colWidths;
|
newNode.__colWidths = [...node.__colWidths];
|
||||||
newNode.__styles = new Map(node.__styles);
|
newNode.__styles = new Map(node.__styles);
|
||||||
return newNode;
|
return newNode;
|
||||||
}
|
}
|
||||||
|
@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode {
|
||||||
updateElementWithCommonBlockProps(tableElement, this);
|
updateElementWithCommonBlockProps(tableElement, this);
|
||||||
|
|
||||||
const colWidths = this.getColWidths();
|
const colWidths = this.getColWidths();
|
||||||
if (colWidths.length > 0) {
|
const colgroup = buildColgroupFromTableWidths(colWidths);
|
||||||
const colgroup = el('colgroup');
|
if (colgroup) {
|
||||||
for (const width of colWidths) {
|
|
||||||
const col = el('col');
|
|
||||||
if (width) {
|
|
||||||
col.style.width = width;
|
|
||||||
}
|
|
||||||
colgroup.append(col);
|
|
||||||
}
|
|
||||||
tableElement.append(colgroup);
|
tableElement.append(colgroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode {
|
||||||
return tableElement;
|
return tableElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(_prevNode: TableNode): boolean {
|
updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean {
|
||||||
return commonPropertiesDifferent(_prevNode, this)
|
applyCommonPropertyChanges(_prevNode, this, dom);
|
||||||
|| this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
|
|
||||||
|| this.__styles.size !== _prevNode.__styles.size
|
if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) {
|
||||||
|| (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
|
const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP');
|
||||||
|
const newColGroup = buildColgroupFromTableWidths(this.__colWidths);
|
||||||
|
if (existingColGroup) {
|
||||||
|
existingColGroup.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newColGroup) {
|
||||||
|
dom.prepend(newColGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) {
|
||||||
|
dom.style.cssText = '';
|
||||||
|
for (const [name, value] of this.__styles.entries()) {
|
||||||
|
dom.style.setProperty(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||||
|
@ -169,7 +181,7 @@ export class TableNode extends CommonBlockNode {
|
||||||
|
|
||||||
getColWidths(): string[] {
|
getColWidths(): string[] {
|
||||||
const self = this.getLatest();
|
const self = this.getLatest();
|
||||||
return self.__colWidths;
|
return [...self.__colWidths];
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyles(): StyleMap {
|
getStyles(): StyleMap {
|
||||||
|
|
|
@ -71,6 +71,7 @@ import {TableDOMTable, TableObserver} from './LexicalTableObserver';
|
||||||
import {$isTableRowNode} from './LexicalTableRowNode';
|
import {$isTableRowNode} from './LexicalTableRowNode';
|
||||||
import {$isTableSelection} from './LexicalTableSelection';
|
import {$isTableSelection} from './LexicalTableSelection';
|
||||||
import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils';
|
import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils';
|
||||||
|
import {$selectOrCreateAdjacent} from "../../utils/nodes";
|
||||||
|
|
||||||
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
|
const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
|
||||||
|
|
||||||
|
@ -915,9 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable {
|
||||||
domRows.length = 0;
|
domRows.length = 0;
|
||||||
|
|
||||||
while (currentNode != null) {
|
while (currentNode != null) {
|
||||||
const nodeMame = currentNode.nodeName;
|
const nodeName = currentNode.nodeName;
|
||||||
|
|
||||||
if (nodeMame === 'TD' || nodeMame === 'TH') {
|
if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') {
|
||||||
|
currentNode = currentNode.nextSibling;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeName === 'TD' || nodeName === 'TH') {
|
||||||
const elem = currentNode as HTMLElement;
|
const elem = currentNode as HTMLElement;
|
||||||
const cell = {
|
const cell = {
|
||||||
elem,
|
elem,
|
||||||
|
@ -1108,7 +1114,7 @@ const selectTableNodeInDirection = (
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tableNode.selectPrevious();
|
$selectOrCreateAdjacent(tableNode, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -1120,7 +1126,7 @@ const selectTableNodeInDirection = (
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tableNode.selectNext();
|
$selectOrCreateAdjacent(tableNode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
TableRowNode,
|
TableRowNode,
|
||||||
} from './LexicalTableRowNode';
|
} from './LexicalTableRowNode';
|
||||||
import {$isTableSelection} from './LexicalTableSelection';
|
import {$isTableSelection} from './LexicalTableSelection';
|
||||||
|
import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
|
||||||
|
|
||||||
export function $createTableNodeWithDimensions(
|
export function $createTableNodeWithDimensions(
|
||||||
rowCount: number,
|
rowCount: number,
|
||||||
|
@ -779,7 +780,7 @@ export function $computeTableMapSkipCellCheck(
|
||||||
return tableMap[row] === undefined || tableMap[row][column] === undefined;
|
return tableMap[row] === undefined || tableMap[row][column] === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridChildren = grid.getChildren();
|
const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node));
|
||||||
for (let i = 0; i < gridChildren.length; i++) {
|
for (let i = 0; i < gridChildren.length; i++) {
|
||||||
const row = gridChildren[i];
|
const row = gridChildren[i];
|
||||||
invariant(
|
invariant(
|
||||||
|
|
|
@ -95,6 +95,21 @@ function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolea
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): boolean {
|
||||||
|
const regex = /https?:\/\/([^?#]*?)\.(png|jpeg|jpg|gif|webp|bmp|avif)/i
|
||||||
|
const text = data.getData('text/plain');
|
||||||
|
if (text && regex.test(text)) {
|
||||||
|
context.editor.update(() => {
|
||||||
|
const image = $createImageNode(text);
|
||||||
|
$insertNodes([image]);
|
||||||
|
image.select();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
|
function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
|
||||||
const editor = context.editor;
|
const editor = context.editor;
|
||||||
return (event: DragEvent): boolean => {
|
return (event: DragEvent): boolean => {
|
||||||
|
@ -138,7 +153,10 @@ function createPasteListener(context: EditorUiContext): (event: ClipboardEvent)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handled = handleMediaInsert(event.clipboardData, context);
|
const handled =
|
||||||
|
handleImageLinkInsert(event.clipboardData, context) ||
|
||||||
|
handleMediaInsert(event.clipboardData, context);
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,15 +13,16 @@ import {
|
||||||
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
|
import {$isImageNode} from "@lexical/rich-text/LexicalImageNode";
|
||||||
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
import {$isMediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||||
import {getLastSelection} from "../utils/selection";
|
import {getLastSelection} from "../utils/selection";
|
||||||
import {$getNearestNodeBlockParent, $getParentOfType} from "../utils/nodes";
|
import {$getNearestNodeBlockParent, $getParentOfType, $selectOrCreateAdjacent} from "../utils/nodes";
|
||||||
import {$setInsetForSelection} from "../utils/lists";
|
import {$setInsetForSelection} from "../utils/lists";
|
||||||
import {$isListItemNode} from "@lexical/list";
|
import {$isListItemNode} from "@lexical/list";
|
||||||
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
|
||||||
|
import {$isDiagramNode} from "../utils/diagrams";
|
||||||
|
|
||||||
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
|
||||||
if (nodes.length === 1) {
|
if (nodes.length === 1) {
|
||||||
const node = nodes[0];
|
const node = nodes[0];
|
||||||
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
|
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node) || $isDiagramNode(node)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,16 +47,21 @@ function deleteSingleSelectedNode(editor: LexicalEditor) {
|
||||||
* Insert a new empty node before/after the selection if the selection contains a single
|
* Insert a new empty node before/after the selection if the selection contains a single
|
||||||
* selected node (like image, media etc...).
|
* selected node (like image, media etc...).
|
||||||
*/
|
*/
|
||||||
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
function insertAdjacentToSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
|
||||||
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
|
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
|
||||||
if (isSingleSelectedNode(selectionNodes)) {
|
if (isSingleSelectedNode(selectionNodes)) {
|
||||||
const node = selectionNodes[0];
|
const node = selectionNodes[0];
|
||||||
const nearestBlock = $getNearestNodeBlockParent(node) || node;
|
const nearestBlock = $getNearestNodeBlockParent(node) || node;
|
||||||
|
const insertBefore = event?.shiftKey === true;
|
||||||
if (nearestBlock) {
|
if (nearestBlock) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const newParagraph = $createParagraphNode();
|
const newParagraph = $createParagraphNode();
|
||||||
|
if (insertBefore) {
|
||||||
|
nearestBlock.insertBefore(newParagraph);
|
||||||
|
} else {
|
||||||
nearestBlock.insertAfter(newParagraph);
|
nearestBlock.insertAfter(newParagraph);
|
||||||
|
}
|
||||||
newParagraph.select();
|
newParagraph.select();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -74,22 +80,9 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event:
|
||||||
}
|
}
|
||||||
|
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
const node = selectionNodes[0];
|
const node = selectionNodes[0];
|
||||||
const nearestBlock = $getNearestNodeBlockParent(node) || node;
|
|
||||||
let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling();
|
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
if (!target) {
|
$selectOrCreateAdjacent(node, after);
|
||||||
target = $createParagraphNode();
|
|
||||||
if (after) {
|
|
||||||
nearestBlock.insertAfter(target)
|
|
||||||
} else {
|
|
||||||
nearestBlock.insertBefore(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
target.selectStart();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -219,7 +212,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void {
|
||||||
}, COMMAND_PRIORITY_LOW);
|
}, COMMAND_PRIORITY_LOW);
|
||||||
|
|
||||||
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
|
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
|
||||||
return insertAfterSingleSelectedNode(context.editor, event)
|
return insertAdjacentToSingleSelectedNode(context.editor, event)
|
||||||
|| moveAfterDetailsOnEmptyLine(context.editor, event);
|
|| moveAfterDetailsOnEmptyLine(context.editor, event);
|
||||||
}, COMMAND_PRIORITY_LOW);
|
}, COMMAND_PRIORITY_LOW);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {EditorDecorator} from "../framework/decorator";
|
import {EditorDecorator} from "../framework/decorator";
|
||||||
import {EditorUiContext} from "../framework/core";
|
import {EditorUiContext} from "../framework/core";
|
||||||
import {BaseSelection} from "lexical";
|
import {BaseSelection, CLICK_COMMAND, COMMAND_PRIORITY_NORMAL} from "lexical";
|
||||||
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
|
||||||
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection";
|
||||||
import {$openDrawingEditorForNode} from "../../utils/diagrams";
|
import {$openDrawingEditorForNode} from "../../utils/diagrams";
|
||||||
|
@ -12,11 +12,17 @@ export class DiagramDecorator extends EditorDecorator {
|
||||||
setup(context: EditorUiContext, element: HTMLElement) {
|
setup(context: EditorUiContext, element: HTMLElement) {
|
||||||
const diagramNode = this.getNode();
|
const diagramNode = this.getNode();
|
||||||
element.classList.add('editor-diagram');
|
element.classList.add('editor-diagram');
|
||||||
element.addEventListener('click', event => {
|
|
||||||
|
context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => {
|
||||||
|
if (!element.contains(event.target as HTMLElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
context.editor.update(() => {
|
context.editor.update(() => {
|
||||||
$selectSingleNode(this.getNode());
|
$selectSingleNode(this.getNode());
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
}, COMMAND_PRIORITY_NORMAL);
|
||||||
|
|
||||||
element.addEventListener('dblclick', event => {
|
element.addEventListener('dblclick', event => {
|
||||||
context.editor.getEditorState().read(() => {
|
context.editor.getEditorState().read(() => {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import {handleDropdown} from "../helpers/dropdowns";
|
|
||||||
import {EditorContainerUiElement, EditorUiElement} from "../core";
|
import {EditorContainerUiElement, EditorUiElement} from "../core";
|
||||||
import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
|
import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
|
@ -8,6 +7,7 @@ export type EditorDropdownButtonOptions = {
|
||||||
showOnHover?: boolean;
|
showOnHover?: boolean;
|
||||||
direction?: 'vertical'|'horizontal';
|
direction?: 'vertical'|'horizontal';
|
||||||
showAside?: boolean;
|
showAside?: boolean;
|
||||||
|
hideOnAction?: boolean;
|
||||||
button: EditorBasicButtonDefinition|EditorButton;
|
button: EditorBasicButtonDefinition|EditorButton;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ const defaultOptions: EditorDropdownButtonOptions = {
|
||||||
showOnHover: false,
|
showOnHover: false,
|
||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
showAside: undefined,
|
showAside: undefined,
|
||||||
|
hideOnAction: true,
|
||||||
button: {label: 'Menu'},
|
button: {label: 'Menu'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
|
||||||
},
|
},
|
||||||
isActive: () => {
|
isActive: () => {
|
||||||
return this.open;
|
return this.open;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
|
||||||
class: 'editor-dropdown-menu-container',
|
class: 'editor-dropdown-menu-container',
|
||||||
}, [button, menu]);
|
}, [button, menu]);
|
||||||
|
|
||||||
handleDropdown({toggle: button, menu : menu,
|
this.getContext().manager.dropdowns.handle({toggle: button, menu : menu,
|
||||||
showOnHover: this.options.showOnHover,
|
showOnHover: this.options.showOnHover,
|
||||||
showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'),
|
showAside: typeof this.options.showAside === 'boolean' ? this.options.showAside : (this.options.direction === 'vertical'),
|
||||||
onOpen : () => {
|
onOpen : () => {
|
||||||
|
@ -76,6 +77,12 @@ export class EditorDropdownButton extends EditorContainerUiElement {
|
||||||
this.getContext().manager.triggerStateUpdateForElement(this.button);
|
this.getContext().manager.triggerStateUpdateForElement(this.button);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
if (this.options.hideOnAction) {
|
||||||
|
this.onEvent('button-action', () => {
|
||||||
|
this.getContext().manager.dropdowns.closeAll();
|
||||||
|
}, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import {EditorUiStateUpdate, EditorContainerUiElement} from "../core";
|
import {EditorUiStateUpdate, EditorContainerUiElement} from "../core";
|
||||||
import {EditorButton} from "../buttons";
|
import {EditorButton} from "../buttons";
|
||||||
import {handleDropdown} from "../helpers/dropdowns";
|
|
||||||
import {el} from "../../../utils/dom";
|
import {el} from "../../../utils/dom";
|
||||||
|
|
||||||
export class EditorFormatMenu extends EditorContainerUiElement {
|
export class EditorFormatMenu extends EditorContainerUiElement {
|
||||||
|
@ -20,7 +19,11 @@ export class EditorFormatMenu extends EditorContainerUiElement {
|
||||||
class: 'editor-format-menu editor-dropdown-menu-container',
|
class: 'editor-format-menu editor-dropdown-menu-container',
|
||||||
}, [toggle, menu]);
|
}, [toggle, menu]);
|
||||||
|
|
||||||
handleDropdown({toggle : toggle, menu : menu});
|
this.getContext().manager.dropdowns.handle({toggle : toggle, menu : menu});
|
||||||
|
|
||||||
|
this.onEvent('button-action', () => {
|
||||||
|
this.getContext().manager.dropdowns.closeAll();
|
||||||
|
}, wrapper);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ export class EditorOverflowContainer extends EditorContainerUiElement {
|
||||||
label: 'More',
|
label: 'More',
|
||||||
icon: moreHorizontal,
|
icon: moreHorizontal,
|
||||||
},
|
},
|
||||||
|
hideOnAction: false,
|
||||||
}, []);
|
}, []);
|
||||||
this.addChildren(this.overflowButton);
|
this.addChildren(this.overflowButton);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,12 @@ export interface EditorBasicButtonDefinition {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
|
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
|
||||||
action: (context: EditorUiContext, button: EditorButton) => void;
|
/**
|
||||||
|
* The action to perform when the button is used.
|
||||||
|
* This can return false to indicate that the completion of the action should
|
||||||
|
* NOT be communicated to parent UI elements, which is what occurs by default.
|
||||||
|
*/
|
||||||
|
action: (context: EditorUiContext, button: EditorButton) => void|false|Promise<void|boolean>;
|
||||||
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
||||||
isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
||||||
setup?: (context: EditorUiContext, button: EditorButton) => void;
|
setup?: (context: EditorUiContext, button: EditorButton) => void;
|
||||||
|
@ -78,7 +83,16 @@ export class EditorButton extends EditorUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onClick() {
|
protected onClick() {
|
||||||
this.definition.action(this.getContext(), this);
|
const result = this.definition.action(this.getContext(), this);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
result.then(result => {
|
||||||
|
if (result === false) {
|
||||||
|
this.emitEvent('button-action');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result !== false) {
|
||||||
|
this.emitEvent('button-action');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateActiveState(selection: BaseSelection|null) {
|
protected updateActiveState(selection: BaseSelection|null) {
|
||||||
|
|
|
@ -67,6 +67,21 @@ export abstract class EditorUiElement {
|
||||||
updateState(state: EditorUiStateUpdate): void {
|
updateState(state: EditorUiStateUpdate): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitEvent(name: string, data: object = {}): void {
|
||||||
|
if (this.dom) {
|
||||||
|
this.dom.dispatchEvent(new CustomEvent('editor::' + name, {detail: data, bubbles: true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(name: string, callback: (data: object) => any, listenTarget: HTMLElement|null = null): void {
|
||||||
|
const target = listenTarget || this.dom;
|
||||||
|
if (target) {
|
||||||
|
target.addEventListener('editor::' + name, ((event: CustomEvent) => {
|
||||||
|
callback(event.detail);
|
||||||
|
}) as EventListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditorContainerUiElement extends EditorUiElement {
|
export class EditorContainerUiElement extends EditorUiElement {
|
||||||
|
|
|
@ -34,57 +34,97 @@ function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDropdown(options: HandleDropdownParams) {
|
export class DropDownManager {
|
||||||
const {menu, toggle, onClose, onOpen, showOnHover, showAside} = options;
|
|
||||||
let clickListener: Function|null = null;
|
|
||||||
|
|
||||||
const hide = () => {
|
protected dropdownOptions: WeakMap<HTMLElement, HandleDropdownParams> = new WeakMap();
|
||||||
|
protected openDropdowns: Set<HTMLElement> = new Set();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.onMenuMouseOver = this.onMenuMouseOver.bind(this);
|
||||||
|
|
||||||
|
window.addEventListener('click', (event: MouseEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
this.closeAllNotContainingElement(target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected closeAllNotContainingElement(element: HTMLElement): void {
|
||||||
|
for (const menu of this.openDropdowns) {
|
||||||
|
if (!menu.parentElement?.contains(element)) {
|
||||||
|
this.closeDropdown(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onMenuMouseOver(event: MouseEvent): void {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
this.closeAllNotContainingElement(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close all open dropdowns.
|
||||||
|
*/
|
||||||
|
public closeAll(): void {
|
||||||
|
for (const menu of this.openDropdowns) {
|
||||||
|
this.closeDropdown(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected closeDropdown(menu: HTMLElement): void {
|
||||||
menu.hidden = true;
|
menu.hidden = true;
|
||||||
menu.style.removeProperty('position');
|
menu.style.removeProperty('position');
|
||||||
menu.style.removeProperty('left');
|
menu.style.removeProperty('left');
|
||||||
menu.style.removeProperty('top');
|
menu.style.removeProperty('top');
|
||||||
if (clickListener) {
|
|
||||||
window.removeEventListener('click', clickListener as EventListener);
|
this.openDropdowns.delete(menu);
|
||||||
}
|
menu.removeEventListener('mouseover', this.onMenuMouseOver);
|
||||||
|
|
||||||
|
const onClose = this.getOptions(menu).onClose;
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const show = () => {
|
protected openDropdown(menu: HTMLElement): void {
|
||||||
|
const {toggle, showAside, onOpen} = this.getOptions(menu);
|
||||||
menu.hidden = false
|
menu.hidden = false
|
||||||
positionMenu(menu, toggle, Boolean(showAside));
|
positionMenu(menu, toggle, Boolean(showAside));
|
||||||
clickListener = (event: MouseEvent) => {
|
|
||||||
if (!toggle.contains(event.target as HTMLElement) && !menu.contains(event.target as HTMLElement)) {
|
this.openDropdowns.add(menu);
|
||||||
hide();
|
menu.addEventListener('mouseover', this.onMenuMouseOver);
|
||||||
}
|
|
||||||
}
|
|
||||||
window.addEventListener('click', clickListener as EventListener);
|
|
||||||
if (onOpen) {
|
if (onOpen) {
|
||||||
onOpen();
|
onOpen();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
protected getOptions(menu: HTMLElement): HandleDropdownParams {
|
||||||
|
const options = this.dropdownOptions.get(menu);
|
||||||
|
if (!options) {
|
||||||
|
throw new Error(`Can't find options for dropdown menu`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add handling for a new dropdown.
|
||||||
|
*/
|
||||||
|
public handle(options: HandleDropdownParams) {
|
||||||
|
const {menu, toggle, showOnHover} = options;
|
||||||
|
|
||||||
|
// Register dropdown
|
||||||
|
this.dropdownOptions.set(menu, options);
|
||||||
|
|
||||||
|
// Configure default events
|
||||||
const toggleShowing = (event: MouseEvent) => {
|
const toggleShowing = (event: MouseEvent) => {
|
||||||
menu.hasAttribute('hidden') ? show() : hide();
|
menu.hasAttribute('hidden') ? this.openDropdown(menu) : this.closeDropdown(menu);
|
||||||
};
|
};
|
||||||
toggle.addEventListener('click', toggleShowing);
|
toggle.addEventListener('click', toggleShowing);
|
||||||
if (showOnHover) {
|
if (showOnHover) {
|
||||||
toggle.addEventListener('mouseenter', toggleShowing);
|
toggle.addEventListener('mouseenter', () => {
|
||||||
}
|
this.openDropdown(menu);
|
||||||
|
|
||||||
menu.parentElement?.addEventListener('mouseleave', (event: MouseEvent) => {
|
|
||||||
|
|
||||||
// Prevent mouseleave hiding if withing the same bounds of the toggle.
|
|
||||||
// Avoids hiding in the event the mouse is interrupted by a high z-index
|
|
||||||
// item like a browser scrollbar.
|
|
||||||
const toggleBounds = toggle.getBoundingClientRect();
|
|
||||||
const withinX = event.clientX <= toggleBounds.right && event.clientX >= toggleBounds.left;
|
|
||||||
const withinY = event.clientY <= toggleBounds.bottom && event.clientY >= toggleBounds.top;
|
|
||||||
const withinToggle = withinX && withinY;
|
|
||||||
|
|
||||||
if (!withinToggle) {
|
|
||||||
hide();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ class TableSelectionHandler {
|
||||||
tableNode,
|
tableNode,
|
||||||
tableElement,
|
tableElement,
|
||||||
this.editor,
|
this.editor,
|
||||||
false,
|
true,
|
||||||
);
|
);
|
||||||
this.tableSelections.set(nodeKey, tableSelection);
|
this.tableSelections.set(nodeKey, tableSelection);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {DecoratorListener} from "lexical/LexicalEditor";
|
||||||
import type {NodeKey} from "lexical/LexicalNode";
|
import type {NodeKey} from "lexical/LexicalNode";
|
||||||
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
import {EditorContextToolbar, EditorContextToolbarDefinition} from "./toolbars";
|
||||||
import {getLastSelection, setLastSelection} from "../../utils/selection";
|
import {getLastSelection, setLastSelection} from "../../utils/selection";
|
||||||
|
import {DropDownManager} from "./helpers/dropdowns";
|
||||||
|
|
||||||
export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
|
export type SelectionChangeHandler = (selection: BaseSelection|null) => void;
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ export class EditorUIManager {
|
||||||
protected activeContextToolbars: EditorContextToolbar[] = [];
|
protected activeContextToolbars: EditorContextToolbar[] = [];
|
||||||
protected selectionChangeHandlers: Set<SelectionChangeHandler> = new Set();
|
protected selectionChangeHandlers: Set<SelectionChangeHandler> = new Set();
|
||||||
|
|
||||||
|
public dropdowns: DropDownManager = new DropDownManager();
|
||||||
|
|
||||||
setContext(context: EditorUiContext) {
|
setContext(context: EditorUiContext) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.setupEventListeners(context);
|
this.setupEventListeners(context);
|
||||||
|
@ -241,6 +244,7 @@ export class EditorUIManager {
|
||||||
if (selectionChange) {
|
if (selectionChange) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection();
|
||||||
|
// console.log('manager::selection', selection);
|
||||||
this.triggerStateUpdate({
|
this.triggerStateUpdate({
|
||||||
editor, selection,
|
editor, selection,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
$isTextNode,
|
$isTextNode,
|
||||||
ElementNode,
|
ElementNode,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode
|
LexicalNode, RangeSelection
|
||||||
} from "lexical";
|
} from "lexical";
|
||||||
import {LexicalNodeMatcher} from "../nodes";
|
import {LexicalNodeMatcher} from "../nodes";
|
||||||
import {$generateNodesFromDOM} from "@lexical/html";
|
import {$generateNodesFromDOM} from "@lexical/html";
|
||||||
|
@ -118,6 +118,22 @@ export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] {
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function $selectOrCreateAdjacent(node: LexicalNode, after: boolean): RangeSelection {
|
||||||
|
const nearestBlock = $getNearestNodeBlockParent(node) || node;
|
||||||
|
let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling()
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
target = $createParagraphNode();
|
||||||
|
if (after) {
|
||||||
|
nearestBlock.insertAfter(target)
|
||||||
|
} else {
|
||||||
|
nearestBlock.insertBefore(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return after ? target.selectStart() : target.selectEnd();
|
||||||
|
}
|
||||||
|
|
||||||
export function nodeHasAlignment(node: object): node is NodeHasAlignment {
|
export function nodeHasAlignment(node: object): node is NodeHasAlignment {
|
||||||
return '__alignment' in node;
|
return '__alignment' in node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
import {$getParentOfType} from "./nodes";
|
import {$getParentOfType} from "./nodes";
|
||||||
import {$getNodeFromSelection} from "./selection";
|
import {$getNodeFromSelection} from "./selection";
|
||||||
import {formatSizeValue} from "./dom";
|
import {el, formatSizeValue} from "./dom";
|
||||||
import {TableMap} from "./table-map";
|
import {TableMap} from "./table-map";
|
||||||
|
|
||||||
function $getTableFromCell(cell: TableCellNode): TableNode|null {
|
function $getTableFromCell(cell: TableCellNode): TableNode|null {
|
||||||
|
@ -140,6 +140,23 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellN
|
||||||
return (widths.length > index) ? widths[index] : '';
|
return (widths.length > index) ? widths[index] : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildColgroupFromTableWidths(colWidths: string[]): HTMLElement|null {
|
||||||
|
if (colWidths.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const colgroup = el('colgroup');
|
||||||
|
for (const width of colWidths) {
|
||||||
|
const col = el('col');
|
||||||
|
if (width) {
|
||||||
|
col.style.width = width;
|
||||||
|
}
|
||||||
|
colgroup.append(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colgroup;
|
||||||
|
}
|
||||||
|
|
||||||
export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
|
export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
|
||||||
if ($isTableSelection(selection)) {
|
if ($isTableSelection(selection)) {
|
||||||
const nodes = selection.getNodes();
|
const nodes = selection.getNodes();
|
||||||
|
|
|
@ -422,7 +422,7 @@ body.editor-is-fullscreen {
|
||||||
.editor-table-marker {
|
.editor-table-marker {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background-color: var(--editor-color-primary);
|
background-color: var(--editor-color-primary);
|
||||||
z-index: 99;
|
z-index: 3;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
&:hover, &.active {
|
&:hover, &.active {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<select name="setting-app-editor" id="setting-app-editor">
|
<select name="setting-app-editor" id="setting-app-editor">
|
||||||
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
|
<option @if(setting('app-editor') === 'wysiwyg') selected @endif value="wysiwyg">WYSIWYG</option>
|
||||||
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
|
<option @if(setting('app-editor') === 'markdown') selected @endif value="markdown">Markdown</option>
|
||||||
<option @if(setting('app-editor') === 'wysiwyg2024') selected @endif value="wysiwyg2024">New WYSIWYG (alpha testing)</option>
|
<option @if(setting('app-editor') === 'wysiwyg2024') selected @endif value="wysiwyg2024">New WYSIWYG (beta testing)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue