diff --git a/lang/en/editor.php b/lang/en/editor.php
index a61b46042..752c6f3f7 100644
--- a/lang/en/editor.php
+++ b/lang/en/editor.php
@@ -13,6 +13,7 @@ return [
'cancel' => 'Cancel',
'save' => 'Save',
'close' => 'Close',
+ 'apply' => 'Apply',
'undo' => 'Undo',
'redo' => 'Redo',
'left' => 'Left',
@@ -147,6 +148,7 @@ return [
'url' => 'URL',
'text_to_display' => 'Text to display',
'title' => 'Title',
+ 'browse_links' => 'Browse links',
'open_link' => 'Open link',
'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window',
diff --git a/resources/icons/editor/color-display.svg b/resources/icons/editor/color-display.svg
new file mode 100644
index 000000000..86be9a7bf
--- /dev/null
+++ b/resources/icons/editor/color-display.svg
@@ -0,0 +1,10 @@
+
diff --git a/resources/icons/editor/color-select.svg b/resources/icons/editor/color-select.svg
new file mode 100644
index 000000000..cef686655
--- /dev/null
+++ b/resources/icons/editor/color-select.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts
index 0f1c0a5d3..f995237a0 100644
--- a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts
+++ b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts
@@ -8,7 +8,6 @@
import type {
BaseSelection,
- ElementFormatType,
LexicalCommand,
LexicalNode,
TextFormatType,
@@ -91,8 +90,6 @@ export const OUTDENT_CONTENT_COMMAND: LexicalCommand = createCommand(
);
export const DROP_COMMAND: LexicalCommand =
createCommand('DROP_COMMAND');
-export const FORMAT_ELEMENT_COMMAND: LexicalCommand =
- createCommand('FORMAT_ELEMENT_COMMAND');
export const DRAGSTART_COMMAND: LexicalCommand =
createCommand('DRAGSTART_COMMAND');
export const DRAGOVER_COMMAND: LexicalCommand =
diff --git a/resources/js/wysiwyg/lexical/core/LexicalConstants.ts b/resources/js/wysiwyg/lexical/core/LexicalConstants.ts
index 82461e74d..55668f1e4 100644
--- a/resources/js/wysiwyg/lexical/core/LexicalConstants.ts
+++ b/resources/js/wysiwyg/lexical/core/LexicalConstants.ts
@@ -6,7 +6,6 @@
*
*/
-import type {ElementFormatType} from './nodes/LexicalElementNode';
import type {
TextDetailType,
TextFormatType,
@@ -111,27 +110,6 @@ export const DETAIL_TYPE_TO_DETAIL: Record = {
unmergeable: IS_UNMERGEABLE,
};
-export const ELEMENT_TYPE_TO_FORMAT: Record<
- Exclude,
- number
-> = {
- center: IS_ALIGN_CENTER,
- end: IS_ALIGN_END,
- justify: IS_ALIGN_JUSTIFY,
- left: IS_ALIGN_LEFT,
- right: IS_ALIGN_RIGHT,
- start: IS_ALIGN_START,
-};
-
-export const ELEMENT_FORMAT_TO_TYPE: Record = {
- [IS_ALIGN_CENTER]: 'center',
- [IS_ALIGN_END]: 'end',
- [IS_ALIGN_JUSTIFY]: 'justify',
- [IS_ALIGN_LEFT]: 'left',
- [IS_ALIGN_RIGHT]: 'right',
- [IS_ALIGN_START]: 'start',
-};
-
export const TEXT_MODE_TO_TYPE: Record = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts
index a6c9b6023..7306e6bca 100644
--- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts
+++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts
@@ -146,6 +146,12 @@ type NodeName = string;
* Output for a DOM conversion.
* Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
* including all its children.
+ *
+ * You can specify a function to run for each converted child (forChild) or on all
+ * the child nodes after the conversion is complete (after).
+ * The key difference here is that forChild runs for every deeply nested child node
+ * of the current node, whereas after will run only once after the
+ * transformation of the node and all its children is complete.
*/
export type DOMConversionOutput = {
after?: (childLexicalNodes: Array) => Array;
@@ -1165,6 +1171,16 @@ export class LexicalNode {
markDirty(): void {
this.getWritable();
}
+
+ /**
+ * Insert the DOM of this node into that of the parent.
+ * Allows this node to implement custom DOM attachment logic.
+ * Boolean result indicates if the insertion was handled by the function.
+ * A true return value prevents default insertion logic from taking place.
+ */
+ insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
+ return false;
+ }
}
function errorOnTypeKlassMismatch(
diff --git a/resources/js/wysiwyg/lexical/core/LexicalReconciler.ts b/resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
index fccf1ae23..297e96ce0 100644
--- a/resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
+++ b/resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
@@ -171,16 +171,21 @@ function $createNode(
}
if (parentDOM !== null) {
- if (insertDOM != null) {
- parentDOM.insertBefore(dom, insertDOM);
- } else {
- // @ts-expect-error: internal field
- const possibleLineBreak = parentDOM.__lexicalLineBreak;
- if (possibleLineBreak != null) {
- parentDOM.insertBefore(dom, possibleLineBreak);
+ const inserted = node?.insertDOMIntoParent(dom, parentDOM);
+
+ if (!inserted) {
+ if (insertDOM != null) {
+ parentDOM.insertBefore(dom, insertDOM);
} else {
- parentDOM.appendChild(dom);
+ // @ts-expect-error: internal field
+ const possibleLineBreak = parentDOM.__lexicalLineBreak;
+
+ if (possibleLineBreak != null) {
+ parentDOM.insertBefore(dom, possibleLineBreak);
+ } else {
+ parentDOM.appendChild(dom);
+ }
}
}
}
diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
index b13bba697..d54a64ce8 100644
--- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
+++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
@@ -37,8 +37,6 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
import {EditorUiContext} from "../../../../ui/framework/core";
import {EditorUIManager} from "../../../../ui/framework/manager";
-import {turtle} from "@codemirror/legacy-modes/mode/turtle";
-
type TestEnv = {
readonly container: HTMLDivElement;
@@ -47,6 +45,9 @@ type TestEnv = {
readonly innerHTML: string;
};
+/**
+ * @deprecated - Consider using `createTestContext` instead within the test case.
+ */
export function initializeUnitTest(
runTests: (testEnv: TestEnv) => void,
editorConfig: CreateEditorArgs = {namespace: 'test', theme: {}},
@@ -795,6 +796,30 @@ export function expectNodeShapeToMatch(editor: LexicalEditor, expected: nodeShap
expect(shape.children).toMatchObject(expected);
}
+/**
+ * Expect a given prop within the JSON editor state structure to be the given value.
+ * Uses dot notation for the provided `propPath`. Example:
+ * 0.5.cat => First child, Sixth child, cat property
+ */
+export function expectEditorStateJSONPropToEqual(editor: LexicalEditor, propPath: string, expected: any) {
+ let currentItem: any = editor.getEditorState().toJSON().root;
+ let currentPath = [];
+ const pathParts = propPath.split('.');
+
+ for (const part of pathParts) {
+ currentPath.push(part);
+ const childAccess = Number.isInteger(Number(part)) && Array.isArray(currentItem.children);
+ const target = childAccess ? currentItem.children : currentItem;
+
+ if (typeof target[part] === 'undefined') {
+ throw new Error(`Could not resolve editor state at path ${currentPath.join('.')}`)
+ }
+ currentItem = target[part];
+ }
+
+ expect(currentItem).toBe(expected);
+}
+
function formatHtml(s: string): string {
return s.replace(/>\s+<').replace(/\s*\n\s*/g, ' ').trim();
}
diff --git a/resources/js/wysiwyg/lexical/core/index.ts b/resources/js/wysiwyg/lexical/core/index.ts
index 5ef926b5a..92cb4a1ca 100644
--- a/resources/js/wysiwyg/lexical/core/index.ts
+++ b/resources/js/wysiwyg/lexical/core/index.ts
@@ -49,15 +49,12 @@ export type {
} from './LexicalNode';
export type {
BaseSelection,
- ElementPointType as ElementPoint,
NodeSelection,
Point,
PointType,
RangeSelection,
- TextPointType as TextPoint,
} from './LexicalSelection';
export type {
- ElementFormatType,
SerializedElementNode,
} from './nodes/LexicalElementNode';
export type {SerializedRootNode} from './nodes/LexicalRootNode';
@@ -87,7 +84,6 @@ export {
DRAGSTART_COMMAND,
DROP_COMMAND,
FOCUS_COMMAND,
- FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts
index 9624af67e..9ad508411 100644
--- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts
+++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts
@@ -46,15 +46,6 @@ export type SerializedElementNode<
SerializedLexicalNode
>;
-export type ElementFormatType =
- | 'left'
- | 'start'
- | 'center'
- | 'right'
- | 'end'
- | 'justify'
- | '';
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface ElementNode {
getTopLevelElement(): ElementNode | null;
diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
index 4a3a48950..7f1b4f305 100644
--- a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
+++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts
@@ -1314,6 +1314,11 @@ const nodeNameToTextFormat: Record = {
function convertTextFormatElement(domNode: HTMLElement): DOMConversionOutput {
const format = nodeNameToTextFormat[domNode.nodeName.toLowerCase()];
+
+ if (format === 'code' && domNode.closest('pre')) {
+ return {node: null};
+ }
+
if (format === undefined) {
return {node: null};
}
diff --git a/resources/js/wysiwyg/lexical/core/shared/invariant.ts b/resources/js/wysiwyg/lexical/core/shared/invariant.ts
index 0e73848ba..8e008c11c 100644
--- a/resources/js/wysiwyg/lexical/core/shared/invariant.ts
+++ b/resources/js/wysiwyg/lexical/core/shared/invariant.ts
@@ -18,9 +18,9 @@ export default function invariant(
return;
}
- throw new Error(
- 'Internal Lexical error: invariant() is meant to be replaced at compile ' +
- 'time. There is no runtime version. Error: ' +
- message,
- );
+ for (const arg of args) {
+ message = (message || '').replace('%s', arg);
+ }
+
+ throw new Error(message);
}
diff --git a/resources/js/wysiwyg/lexical/html/index.ts b/resources/js/wysiwyg/lexical/html/index.ts
index 5c3cb6cce..5018e10b4 100644
--- a/resources/js/wysiwyg/lexical/html/index.ts
+++ b/resources/js/wysiwyg/lexical/html/index.ts
@@ -11,7 +11,6 @@ import type {
DOMChildConversion,
DOMConversion,
DOMConversionFn,
- ElementFormatType,
LexicalEditor,
LexicalNode,
} from 'lexical';
@@ -58,6 +57,7 @@ export function $generateNodesFromDOM(
}
}
}
+
$unwrapArtificalNodes(allArtificialNodes);
return lexicalNodes;
@@ -324,8 +324,6 @@ function wrapContinuousInlines(
nodes: Array,
createWrapperFn: () => ElementNode,
): Array {
- const textAlign = (domNode as HTMLElement).style
- .textAlign as ElementFormatType;
const out: Array = [];
let continuousInlines: Array = [];
// wrap contiguous inline child nodes in para
diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts
index cbe691848..49ba7754c 100644
--- a/resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts
+++ b/resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts
@@ -145,7 +145,14 @@ export class CodeBlockNode extends DecoratorNode {
node.setId(element.id);
}
- return { node };
+ return {
+ node,
+ after(childNodes): LexicalNode[] {
+ // Remove any child nodes that may get parsed since we're manually
+ // controlling the code contents.
+ return [];
+ },
+ };
},
priority: 3,
};
diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
index a675665ac..81fb96a93 100644
--- a/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
+++ b/resources/js/wysiwyg/lexical/rich-text/LexicalMediaNode.ts
@@ -16,6 +16,7 @@ import {
} from "lexical/nodes/common";
import {$selectSingleNode} from "../../utils/selection";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
+import * as url from "node:url";
export type MediaNodeTag = 'iframe' | 'embed' | 'object' | 'video' | 'audio';
export type MediaNodeSource = {
@@ -343,11 +344,55 @@ export function $createMediaNodeFromHtml(html: string): MediaNode | null {
return domElementToNode(tag as MediaNodeTag, el);
}
+interface UrlPattern {
+ readonly regex: RegExp;
+ readonly w: number;
+ readonly h: number;
+ readonly url: string;
+}
+
+/**
+ * These patterns originate from the tinymce/tinymce project.
+ * https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts
+ * License: MIT Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
+ * License Link: https://github.com/tinymce/tinymce/blob/584a150679669859a528828e5d2910a083b1d911/LICENSE.TXT
+ */
+const urlPatterns: UrlPattern[] = [
+ {
+ regex: /.*?youtu\.be\/([\w\-_\?&=.]+)/i,
+ w: 560, h: 314,
+ url: 'https://www.youtube.com/embed/$1',
+ },
+ {
+ regex: /.*youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?.*/i,
+ w: 560, h: 314,
+ url: 'https://www.youtube.com/embed/$2?$4',
+ },
+ {
+ regex: /.*youtube.com\/embed\/([a-z0-9\?&=\-_]+).*/i,
+ w: 560, h: 314,
+ url: 'https://www.youtube.com/embed/$1',
+ },
+];
+
const videoExtensions = ['mp4', 'mpeg', 'm4v', 'm4p', 'mov'];
const audioExtensions = ['3gp', 'aac', 'flac', 'mp3', 'm4a', 'ogg', 'wav', 'webm'];
const iframeExtensions = ['html', 'htm', 'php', 'asp', 'aspx', ''];
export function $createMediaNodeFromSrc(src: string): MediaNode {
+
+ for (const pattern of urlPatterns) {
+ const match = src.match(pattern.regex);
+ if (match) {
+ const newSrc = src.replace(pattern.regex, pattern.url);
+ const node = new MediaNode('iframe');
+ node.setSrc(newSrc);
+ node.setHeight(pattern.h);
+ node.setWidth(pattern.w);
+ return node;
+ }
+ }
+
let nodeTag: MediaNodeTag = 'iframe';
const srcEnd = src.split('?')[0].split('/').pop() || '';
const srcEndSplit = srcEnd.split('.');
@@ -360,7 +405,9 @@ export function $createMediaNodeFromSrc(src: string): MediaNode {
nodeTag = 'embed';
}
- return new MediaNode(nodeTag);
+ const node = new MediaNode(nodeTag);
+ node.setSrc(src);
+ return node;
}
export function $isMediaNode(node: LexicalNode | null | undefined): node is MediaNode {
diff --git a/resources/js/wysiwyg/lexical/rich-text/index.ts b/resources/js/wysiwyg/lexical/rich-text/index.ts
index c585c028a..477fdd781 100644
--- a/resources/js/wysiwyg/lexical/rich-text/index.ts
+++ b/resources/js/wysiwyg/lexical/rich-text/index.ts
@@ -8,7 +8,6 @@
import type {
CommandPayloadType,
- ElementFormatType,
LexicalCommand,
LexicalEditor,
PasteCommandType,
@@ -44,7 +43,6 @@ import {
DRAGSTART_COMMAND,
DROP_COMMAND,
ElementNode,
- FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
INSERT_LINE_BREAK_COMMAND,
INSERT_PARAGRAPH_COMMAND,
@@ -285,25 +283,6 @@ export function registerRichText(editor: LexicalEditor): () => void {
},
COMMAND_PRIORITY_EDITOR,
),
- editor.registerCommand(
- FORMAT_ELEMENT_COMMAND,
- (format) => {
- const selection = $getSelection();
- if (!$isRangeSelection(selection) && !$isNodeSelection(selection)) {
- return false;
- }
- const nodes = selection.getNodes();
- for (const node of nodes) {
- const element = $findMatchingParent(
- node,
- (parentNode): parentNode is ElementNode =>
- $isElementNode(parentNode) && !parentNode.isInline(),
- );
- }
- return true;
- },
- COMMAND_PRIORITY_EDITOR,
- ),
editor.registerCommand(
INSERT_LINE_BREAK_COMMAND,
(selectStart) => {
diff --git a/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts b/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts
new file mode 100644
index 000000000..d9d83562c
--- /dev/null
+++ b/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts
@@ -0,0 +1,92 @@
+import {
+ $createTextNode,
+ DOMConversionMap,
+ DOMExportOutput,
+ EditorConfig,
+ ElementNode,
+ LexicalEditor,
+ LexicalNode,
+ SerializedElementNode
+} from "lexical";
+import {TableNode} from "@lexical/table/LexicalTableNode";
+
+
+export class CaptionNode extends ElementNode {
+ static getType(): string {
+ return 'caption';
+ }
+
+ static clone(node: CaptionNode): CaptionNode {
+ return new CaptionNode(node.__key);
+ }
+
+ createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
+ return document.createElement('caption');
+ }
+
+ updateDOM(_prevNode: unknown, _dom: HTMLElement, _config: EditorConfig): boolean {
+ return false;
+ }
+
+ isParentRequired(): true {
+ return true;
+ }
+
+ canBeEmpty(): boolean {
+ return false;
+ }
+
+ exportJSON(): SerializedElementNode {
+ return {
+ ...super.exportJSON(),
+ type: 'caption',
+ version: 1,
+ };
+ }
+
+ insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
+ parentDOM.insertBefore(nodeDOM, parentDOM.firstChild);
+ return true;
+ }
+
+ static importJSON(serializedNode: SerializedElementNode): CaptionNode {
+ return $createCaptionNode();
+ }
+
+ static importDOM(): DOMConversionMap | null {
+ return {
+ caption: (node: Node) => ({
+ conversion(domNode: Node) {
+ return {
+ node: $createCaptionNode(),
+ }
+ },
+ priority: 0,
+ }),
+ };
+ }
+}
+
+export function $createCaptionNode(): CaptionNode {
+ return new CaptionNode();
+}
+
+export function $isCaptionNode(node: LexicalNode | null | undefined): node is CaptionNode {
+ return node instanceof CaptionNode;
+}
+
+export function $tableHasCaption(table: TableNode): boolean {
+ for (const child of table.getChildren()) {
+ if ($isCaptionNode(child)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function $addCaptionToTable(table: TableNode, text: string = ''): void {
+ const caption = $createCaptionNode();
+ const textNode = $createTextNode(text || ' ');
+ caption.append(textNode);
+ table.append(caption);
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
index 9443747a6..a10361475 100644
--- a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
+++ b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
@@ -139,6 +139,8 @@ export class TableNode extends CommonBlockNode {
for (const child of Array.from(tableElement.children)) {
if (child.nodeName === 'TR') {
tBody.append(child);
+ } else if (child.nodeName === 'CAPTION') {
+ newElement.insertBefore(child, newElement.firstChild);
} else {
newElement.append(child);
}
diff --git a/resources/js/wysiwyg/nodes.ts b/resources/js/wysiwyg/nodes.ts
index 8a47f322d..c1db0f086 100644
--- a/resources/js/wysiwyg/nodes.ts
+++ b/resources/js/wysiwyg/nodes.ts
@@ -18,6 +18,7 @@ import {EditorUiContext} from "./ui/framework/core";
import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
+import {CaptionNode} from "@lexical/table/LexicalCaptionNode";
/**
* Load the nodes for lexical.
@@ -32,6 +33,7 @@ export function getNodesForPageEditor(): (KlassConstructor |
TableNode,
TableRowNode,
TableCellNode,
+ CaptionNode,
ImageNode, // TODO - Alignment
HorizontalRuleNode,
DetailsNode,
diff --git a/resources/js/wysiwyg/services/__tests__/auto-links.test.ts b/resources/js/wysiwyg/services/__tests__/auto-links.test.ts
index 30dc92565..add61c495 100644
--- a/resources/js/wysiwyg/services/__tests__/auto-links.test.ts
+++ b/resources/js/wysiwyg/services/__tests__/auto-links.test.ts
@@ -1,91 +1,76 @@
-import {initializeUnitTest} from "lexical/__tests__/utils";
-import {SerializedLinkNode} from "@lexical/link";
+import {
+ createTestContext,
+ dispatchKeydownEventForNode, expectEditorStateJSONPropToEqual,
+ expectNodeShapeToMatch
+} from "lexical/__tests__/utils";
import {
$getRoot,
ParagraphNode,
- SerializedParagraphNode,
- SerializedTextNode,
TextNode
} from "lexical";
import {registerAutoLinks} from "../auto-links";
describe('Auto-link service tests', () => {
- initializeUnitTest((testEnv) => {
+ test('space after link in text', async () => {
+ const {editor} = createTestContext();
+ registerAutoLinks(editor);
+ let pNode!: ParagraphNode;
- test('space after link in text', async () => {
- const {editor} = testEnv;
+ editor.updateAndCommit(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Some https://example.com?test=true text');
+ pNode.append(text);
+ $getRoot().append(pNode);
- registerAutoLinks(editor);
- let pNode!: ParagraphNode;
-
- editor.update(() => {
- pNode = new ParagraphNode();
- const text = new TextNode('Some https://example.com?test=true text');
- pNode.append(text);
- $getRoot().append(pNode);
-
- text.select(34, 34);
- });
-
- editor.commitUpdates();
-
- const pDomEl = editor.getElementByKey(pNode.getKey());
- const event = new KeyboardEvent('keydown', {
- bubbles: true,
- cancelable: true,
- key: ' ',
- keyCode: 62,
- });
- pDomEl?.dispatchEvent(event);
-
- editor.commitUpdates();
-
- const paragraph = editor!.getEditorState().toJSON().root
- .children[0] as SerializedParagraphNode;
- expect(paragraph.children[1].type).toBe('link');
-
- const link = paragraph.children[1] as SerializedLinkNode;
- expect(link.url).toBe('https://example.com?test=true');
- const linkText = link.children[0] as SerializedTextNode;
- expect(linkText.text).toBe('https://example.com?test=true');
+ text.select(34, 34);
});
- test('enter after link in text', async () => {
- const {editor} = testEnv;
+ dispatchKeydownEventForNode(pNode, editor, ' ');
- registerAutoLinks(editor);
- let pNode!: ParagraphNode;
+ expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
+ expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
+ });
- editor.update(() => {
- pNode = new ParagraphNode();
- const text = new TextNode('Some https://example.com?test=true text');
- pNode.append(text);
- $getRoot().append(pNode);
+ test('space after link at end of line', async () => {
+ const {editor} = createTestContext();
+ registerAutoLinks(editor);
+ let pNode!: ParagraphNode;
- text.select(34, 34);
- });
+ editor.updateAndCommit(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Some https://example.com?test=true');
+ pNode.append(text);
+ $getRoot().append(pNode);
- editor.commitUpdates();
-
- const pDomEl = editor.getElementByKey(pNode.getKey());
- const event = new KeyboardEvent('keydown', {
- bubbles: true,
- cancelable: true,
- key: 'Enter',
- keyCode: 66,
- });
- pDomEl?.dispatchEvent(event);
-
- editor.commitUpdates();
-
- const paragraph = editor!.getEditorState().toJSON().root
- .children[0] as SerializedParagraphNode;
- expect(paragraph.children[1].type).toBe('link');
-
- const link = paragraph.children[1] as SerializedLinkNode;
- expect(link.url).toBe('https://example.com?test=true');
- const linkText = link.children[0] as SerializedTextNode;
- expect(linkText.text).toBe('https://example.com?test=true');
+ text.selectEnd();
});
+
+ dispatchKeydownEventForNode(pNode, editor, ' ');
+
+ expectNodeShapeToMatch(editor, [{type: 'paragraph', children: [
+ {text: 'Some '},
+ {type: 'link', children: [{text: 'https://example.com?test=true'}]}
+ ]}]);
+ expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
+ });
+
+ test('enter after link in text', async () => {
+ const {editor} = createTestContext();
+ registerAutoLinks(editor);
+ let pNode!: ParagraphNode;
+
+ editor.updateAndCommit(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Some https://example.com?test=true text');
+ pNode.append(text);
+ $getRoot().append(pNode);
+
+ text.select(34, 34);
+ });
+
+ dispatchKeydownEventForNode(pNode, editor, 'Enter');
+
+ expectEditorStateJSONPropToEqual(editor, '0.1.url', 'https://example.com?test=true');
+ expectEditorStateJSONPropToEqual(editor, '0.1.0.text', 'https://example.com?test=true');
});
});
\ No newline at end of file
diff --git a/resources/js/wysiwyg/services/auto-links.ts b/resources/js/wysiwyg/services/auto-links.ts
index 1c3b1c730..62cd45994 100644
--- a/resources/js/wysiwyg/services/auto-links.ts
+++ b/resources/js/wysiwyg/services/auto-links.ts
@@ -43,7 +43,7 @@ function handlePotentialLinkEvent(node: TextNode, selection: BaseSelection, edit
linkNode.append(new TextNode(textSegment));
const splits = node.splitText(startIndex, cursorPoint);
- const targetIndex = splits.length === 3 ? 1 : 0;
+ const targetIndex = startIndex > 0 ? 1 : 0;
const targetText = splits[targetIndex];
if (targetText) {
targetText.replace(linkNode);
diff --git a/resources/js/wysiwyg/testing.md b/resources/js/wysiwyg/testing.md
new file mode 100644
index 000000000..7b272c606
--- /dev/null
+++ b/resources/js/wysiwyg/testing.md
@@ -0,0 +1,55 @@
+# Testing Guidance
+
+This is testing guidance specific for this Lexical-based WYSIWYG editor.
+There is a lot of pre-existing test code carried over form the fork of lexical, but since there we've added a range of helpers and altered how testing can be done to make things a bit simpler and aligned with how we run tests.
+
+This document is an attempt to document the new best options for added tests with an aim for standardisation on these approaches going forward.
+
+## Utils Location
+
+Most core test utils can be found in the file at path: resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts
+
+## Test Example
+
+This is an example of a typical test using the common modern utilities to help perform actions or assertions. Comments are for this example only, and are not expected in actual test files.
+
+```ts
+import {
+ createTestContext,
+ dispatchKeydownEventForNode,
+ expectEditorStateJSONPropToEqual,
+ expectNodeShapeToMatch
+} from "lexical/__tests__/utils";
+import {
+ $getRoot,
+ ParagraphNode,
+ TextNode
+} from "lexical";
+
+describe('A specific service or file or function', () => {
+ test('it does thing', async () => {
+ // Create the editor context and get an editor reference
+ const {editor} = createTestContext();
+
+ // Run an action within the editor.
+ let pNode: ParagraphNode;
+ editor.updateAndCommit(() => {
+ pNode = new ParagraphNode();
+ const text = new TextNode('Hello!');
+ pNode.append(text);
+ $getRoot().append(pNode);
+ });
+
+ // Dispatch key events via the DOM
+ dispatchKeydownEventForNode(pNode!, editor, ' ');
+
+ // Check the shape (and text) of the resulting state
+ expectNodeShapeToMatch(editor, [{type: 'paragraph', children: [
+ {text: 'Hello!'},
+ ]}]);
+
+ // Check specific props in the resulting JSON state
+ expectEditorStateJSONPropToEqual(editor, '0.0.text', 'Hello!');
+ });
+});
+```
\ No newline at end of file
diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
deleted file mode 100644
index 817a235a7..000000000
--- a/resources/js/wysiwyg/todo.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Lexical based editor todo
-
-## In progress
-
-Reorg
- - Merge custom nodes into original nodes
- - Reduce down to use CommonBlockNode where possible
- - Remove existing formatType/ElementFormatType references (replaced with alignment).
- - Remove existing indent references (replaced with inset).
-
-## Main Todo
-
-//
-
-## Secondary Todo
-
-- Color picker support in table form color fields
-- Color picker for color controls
-- Table caption text support
-- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
-- Deep check of translation coverage
-- About button & view
-- Mobile display and handling
-
-## Bugs
-
-//
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/controls.ts b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts
index 5e3200539..6c22d3faa 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/controls.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts
@@ -57,7 +57,7 @@ export const redo: EditorButtonDefinition = {
export const source: EditorButtonDefinition = {
- label: 'Source',
+ label: 'Source code',
icon: sourceIcon,
async action(context: EditorUiContext) {
const modal = context.manager.createModal('source');
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
index c3726acf0..c5b7ad29a 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts
@@ -12,6 +12,8 @@ import subscriptIcon from "@icons/editor/subscript.svg";
import codeIcon from "@icons/editor/code.svg";
import formatClearIcon from "@icons/editor/format-clear.svg";
import {$selectionContainsTextFormat} from "../../../utils/selection";
+import {$patchStyleText} from "@lexical/selection";
+import {context} from "esbuild";
function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
return {
@@ -32,6 +34,18 @@ export const underline: EditorButtonDefinition = buildFormatButton('Underline',
export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon};
export const highlightColor: EditorBasicButtonDefinition = {label: 'Background color', icon: highlightIcon};
+function colorAction(context: EditorUiContext, property: string, color: string): void {
+ context.editor.update(() => {
+ const selection = $getSelection();
+ if (selection) {
+ $patchStyleText(selection, {[property]: color || null});
+ }
+ });
+}
+
+export const textColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color);
+export const highlightColorAction = (color: string, context: EditorUiContext) => colorAction(context, 'color', color);
+
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
index 6612c0dc4..4eb4c5a4e 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts
@@ -32,7 +32,7 @@ import {
} from "../../../utils/selection";
import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
-import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects";
+import {$showDetailsForm, $showImageForm, $showLinkForm, $showMediaForm} from "../forms/objects";
import {formatCodeBlock} from "../../../utils/formats";
export const link: EditorButtonDefinition = {
@@ -165,27 +165,14 @@ export const diagramManager: EditorButtonDefinition = {
};
export const media: EditorButtonDefinition = {
- label: 'Insert/edit Media',
+ label: 'Insert/edit media',
icon: mediaIcon,
action(context: EditorUiContext) {
- const mediaModal = context.manager.createModal('media');
-
context.editor.getEditorState().read(() => {
const selection = $getSelection();
const selectedNode = $getNodeFromSelection(selection, $isMediaNode) as MediaNode | null;
- let formDefaults = {};
- if (selectedNode) {
- const nodeAttrs = selectedNode.getAttributes();
- formDefaults = {
- src: nodeAttrs.src || nodeAttrs.data || '',
- width: nodeAttrs.width,
- height: nodeAttrs.height,
- embed: '',
- }
- }
-
- mediaModal.show(formDefaults);
+ $showMediaForm(selectedNode, context);
});
},
isActive(selection: BaseSelection | null): boolean {
diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
index 21d333c3a..0effdc171 100644
--- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts
+++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts
@@ -186,6 +186,23 @@ export const link: EditorFormDefinition = {
],
};
+export function $showMediaForm(media: MediaNode|null, context: EditorUiContext): void {
+ const mediaModal = context.manager.createModal('media');
+
+ let formDefaults = {};
+ if (media) {
+ const nodeAttrs = media.getAttributes();
+ formDefaults = {
+ src: nodeAttrs.src || nodeAttrs.data || '',
+ width: nodeAttrs.width,
+ height: nodeAttrs.height,
+ embed: '',
+ }
+ }
+
+ mediaModal.show(formDefaults);
+}
+
export const media: EditorFormDefinition = {
submitText: 'Save',
async action(formData, context: EditorUiContext) {
@@ -215,12 +232,19 @@ export const media: EditorFormDefinition = {
const height = (formData.get('height') || '').toString().trim();
const width = (formData.get('width') || '').toString().trim();
- const updateNode = selectedNode || $createMediaNodeFromSrc(src);
- updateNode.setSrc(src);
- updateNode.setWidthAndHeight(width, height);
- if (!selectedNode) {
- $insertNodes([updateNode]);
+ // Update existing
+ if (selectedNode) {
+ selectedNode.setSrc(src);
+ selectedNode.setWidthAndHeight(width, height);
+ return;
}
+
+ // Insert new
+ const node = $createMediaNodeFromSrc(src);
+ if (width || height) {
+ node.setWidthAndHeight(width, height);
+ }
+ $insertNodes([node]);
});
return true;
diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts
index 63fa24c80..5b484310d 100644
--- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts
+++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts
@@ -1,6 +1,6 @@
import {
EditorFormDefinition,
- EditorFormFieldDefinition,
+ EditorFormFieldDefinition, EditorFormFields,
EditorFormTabs,
EditorSelectFormFieldDefinition
} from "../../framework/forms";
@@ -17,6 +17,8 @@ import {
import {formatSizeValue} from "../../../utils/dom";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {CommonBlockAlignment} from "lexical/nodes/common";
+import {colorFieldBuilder} from "../../framework/blocks/color-field";
+import {$addCaptionToTable, $isCaptionNode, $tableHasCaption} from "@lexical/table/LexicalCaptionNode";
const borderStyleInput: EditorSelectFormFieldDefinition = {
label: 'Border style',
@@ -145,15 +147,15 @@ export const cellProperties: EditorFormDefinition = {
} as EditorSelectFormFieldDefinition,
];
- const advancedFields: EditorFormFieldDefinition[] = [
+ const advancedFields: EditorFormFields = [
{
label: 'Border width', // inline-style: border-width
name: 'border_width',
type: 'text',
},
borderStyleInput, // inline-style: border-style
- borderColorInput, // inline-style: border-color
- backgroundColorInput, // inline-style: background-color
+ colorFieldBuilder(borderColorInput),
+ colorFieldBuilder(backgroundColorInput),
];
return new EditorFormTabs([
@@ -210,14 +212,15 @@ export const rowProperties: EditorFormDefinition = {
type: 'text',
},
borderStyleInput, // style on tr: height
- borderColorInput, // style on tr: height
- backgroundColorInput, // style on tr: height
+ colorFieldBuilder(borderColorInput),
+ colorFieldBuilder(backgroundColorInput),
],
};
export function $showTablePropertiesForm(table: TableNode, context: EditorUiContext): EditorFormModal {
const styles = table.getStyles();
const modalForm = context.manager.createModal('table_properties');
+
modalForm.show({
width: styles.get('width') || '',
height: styles.get('height') || '',
@@ -227,7 +230,7 @@ export function $showTablePropertiesForm(table: TableNode, context: EditorUiCont
border_style: styles.get('border-style') || '',
border_color: styles.get('border-color') || '',
background_color: styles.get('background-color') || '',
- // caption: '', TODO
+ caption: $tableHasCaption(table) ? 'true' : '',
align: table.getAlignment(),
});
return modalForm;
@@ -264,7 +267,17 @@ export const tableProperties: EditorFormDefinition = {
});
}
- // TODO - cell caption
+ const showCaption = Boolean(formData.get('caption')?.toString() || '');
+ const hasCaption = $tableHasCaption(table);
+ if (showCaption && !hasCaption) {
+ $addCaptionToTable(table, context.translate('Caption'));
+ } else if (!showCaption && hasCaption) {
+ for (const child of table.getChildren()) {
+ if ($isCaptionNode(child)) {
+ child.remove();
+ }
+ }
+ }
});
return true;
},
@@ -298,17 +311,17 @@ export const tableProperties: EditorFormDefinition = {
type: 'text',
},
{
- label: 'caption', // Caption element
+ label: 'Show caption', // Caption element
name: 'caption',
- type: 'text', // TODO -
+ type: 'checkbox',
},
alignmentInput, // alignment class
];
- const advancedFields: EditorFormFieldDefinition[] = [
- borderStyleInput, // Style - border-style
- borderColorInput, // Style - border-color
- backgroundColorInput, // Style - background-color
+ const advancedFields: EditorFormFields = [
+ borderStyleInput,
+ colorFieldBuilder(borderColorInput),
+ colorFieldBuilder(backgroundColorInput),
];
return new EditorFormTabs([
diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts
index 61baa3c32..b09a7530f 100644
--- a/resources/js/wysiwyg/ui/defaults/toolbars.ts
+++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts
@@ -44,11 +44,11 @@ import {
} from "./buttons/block-formats";
import {
bold, clearFormating, code,
- highlightColor,
+ highlightColor, highlightColorAction,
italic,
strikethrough, subscript,
superscript,
- textColor,
+ textColor, textColorAction,
underline
} from "./buttons/inline-formats";
import {
@@ -114,10 +114,10 @@ export function getMainEditorFullToolbar(context: EditorUiContext): EditorContai
new EditorButton(italic),
new EditorButton(underline),
new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [
- new EditorColorPicker('color'),
+ new EditorColorPicker(textColorAction),
]),
new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [
- new EditorColorPicker('background-color'),
+ new EditorColorPicker(highlightColorAction),
]),
new EditorButton(strikethrough),
new EditorButton(superscript),
diff --git a/resources/js/wysiwyg/ui/framework/blocks/color-field.ts b/resources/js/wysiwyg/ui/framework/blocks/color-field.ts
new file mode 100644
index 000000000..8c8f167d9
--- /dev/null
+++ b/resources/js/wysiwyg/ui/framework/blocks/color-field.ts
@@ -0,0 +1,56 @@
+import {EditorContainerUiElement, EditorUiBuilderDefinition, EditorUiContext} from "../core";
+import {EditorFormField, EditorFormFieldDefinition} from "../forms";
+import {EditorColorPicker} from "./color-picker";
+import {EditorDropdownButton} from "./dropdown-button";
+
+import colorDisplayIcon from "@icons/editor/color-display.svg"
+
+export class EditorColorField extends EditorContainerUiElement {
+ protected input: EditorFormField;
+ protected pickerButton: EditorDropdownButton;
+
+ constructor(input: EditorFormField) {
+ super([]);
+
+ this.input = input;
+
+ this.pickerButton = new EditorDropdownButton({
+ button: { icon: colorDisplayIcon, label: 'Select color'}
+ }, [
+ new EditorColorPicker(this.onColorSelect.bind(this))
+ ]);
+ this.addChildren(this.pickerButton, this.input);
+ }
+
+ protected buildDOM(): HTMLElement {
+ const dom = this.input.getDOMElement();
+ dom.append(this.pickerButton.getDOMElement());
+ dom.classList.add('editor-color-field-container');
+
+ const field = dom.querySelector('input') as HTMLInputElement;
+ field.addEventListener('change', () => {
+ this.setIconColor(field.value);
+ });
+
+ return dom;
+ }
+
+ onColorSelect(color: string, context: EditorUiContext): void {
+ this.input.setValue(color);
+ }
+
+ setIconColor(color: string) {
+ const icon = this.getDOMElement().querySelector('svg .editor-icon-color-display');
+ if (icon) {
+ icon.setAttribute('fill', color || 'url(#pattern2)');
+ }
+ }
+}
+
+export function colorFieldBuilder(field: EditorFormFieldDefinition): EditorUiBuilderDefinition {
+ return {
+ build() {
+ return new EditorColorField(new EditorFormField(field));
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts b/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts
index b068fb4f0..8e62a0e5e 100644
--- a/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts
+++ b/resources/js/wysiwyg/ui/framework/blocks/color-picker.ts
@@ -1,9 +1,9 @@
-import {EditorUiElement} from "../core";
-import {$getSelection} from "lexical";
-import {$patchStyleText} from "@lexical/selection";
+import {EditorUiContext, EditorUiElement} from "../core";
import {el} from "../../../utils/dom";
import removeIcon from "@icons/editor/color-clear.svg";
+import selectIcon from "@icons/editor/color-select.svg";
+import {uniqueIdSmall} from "../../../../services/util";
const colorChoices = [
'#000000',
@@ -34,18 +34,24 @@ const colorChoices = [
'#34495E',
];
+const storageKey = 'bs-lexical-custom-colors';
+
+export type EditorColorPickerCallback = (color: string, context: EditorUiContext) => void;
+
export class EditorColorPicker extends EditorUiElement {
- protected styleProperty: string;
+ protected callback: EditorColorPickerCallback;
- constructor(styleProperty: string) {
+ constructor(callback: EditorColorPickerCallback) {
super();
- this.styleProperty = styleProperty;
+ this.callback = callback;
}
buildDOM(): HTMLElement {
+ const id = uniqueIdSmall();
- const colorOptions = colorChoices.map(choice => {
+ const allChoices = [...colorChoices, ...this.getCustomColorChoices()];
+ const colorOptions = allChoices.map(choice => {
return el('div', {
class: 'editor-color-select-option',
style: `background-color: ${choice}`,
@@ -57,11 +63,30 @@ export class EditorColorPicker extends EditorUiElement {
const removeButton = el('div', {
class: 'editor-color-select-option',
'data-color': '',
- title: 'Clear color',
+ title: this.getContext().translate('Remove color'),
}, []);
removeButton.innerHTML = removeIcon;
colorOptions.push(removeButton);
+ const selectButton = el('label', {
+ class: 'editor-color-select-option',
+ for: `color-select-${id}`,
+ 'data-color': '',
+ title: this.getContext().translate('Custom color'),
+ }, []);
+ selectButton.innerHTML = selectIcon;
+ colorOptions.push(selectButton);
+
+ const input = el('input', {type: 'color', hidden: 'true', id: `color-select-${id}`}) as HTMLInputElement;
+ colorOptions.push(input);
+ input.addEventListener('change', e => {
+ if (input.value) {
+ this.storeCustomColorChoice(input.value);
+ this.setColor(input.value);
+ this.rebuildDOM();
+ }
+ });
+
const colorRows = [];
for (let i = 0; i < colorOptions.length; i+=5) {
const options = colorOptions.slice(i, i + 5);
@@ -79,16 +104,33 @@ export class EditorColorPicker extends EditorUiElement {
return wrapper;
}
+ storeCustomColorChoice(color: string) {
+ if (colorChoices.includes(color)) {
+ return;
+ }
+
+ const customColors: string[] = this.getCustomColorChoices();
+ if (customColors.includes(color)) {
+ return;
+ }
+
+ customColors.push(color);
+ window.localStorage.setItem(storageKey, JSON.stringify(customColors));
+ }
+
+ getCustomColorChoices(): string[] {
+ return JSON.parse(window.localStorage.getItem(storageKey) || '[]');
+ }
+
onClick(event: MouseEvent) {
const colorEl = (event.target as HTMLElement).closest('[data-color]') as HTMLElement;
if (!colorEl) return;
const color = colorEl.dataset.color as string;
- this.getContext().editor.update(() => {
- const selection = $getSelection();
- if (selection) {
- $patchStyleText(selection, {[this.styleProperty]: color || null});
- }
- });
+ this.setColor(color);
+ }
+
+ setColor(color: string) {
+ this.callback(color, this.getContext());
}
}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/framework/blocks/link-field.ts b/resources/js/wysiwyg/ui/framework/blocks/link-field.ts
index f88b22c3f..880238a9a 100644
--- a/resources/js/wysiwyg/ui/framework/blocks/link-field.ts
+++ b/resources/js/wysiwyg/ui/framework/blocks/link-field.ts
@@ -44,7 +44,6 @@ export class LinkField extends EditorContainerUiElement {
updateFormFromHeader(header: HeadingNode) {
this.getHeaderIdAndText(header).then(({id, text}) => {
- console.log('updating form', id, text);
const modal = this.getContext().manager.getActiveModal('link');
if (modal) {
modal.getForm().setValues({
@@ -60,7 +59,6 @@ export class LinkField extends EditorContainerUiElement {
return new Promise((res) => {
this.getContext().editor.update(() => {
let id = header.getId();
- console.log('header', id, header.__id);
if (!id) {
id = 'header-' + uniqueIdSmall();
header.setId(id);
diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts
index 3433b96e8..90ce4ebf9 100644
--- a/resources/js/wysiwyg/ui/framework/core.ts
+++ b/resources/js/wysiwyg/ui/framework/core.ts
@@ -53,6 +53,13 @@ export abstract class EditorUiElement {
return this.dom;
}
+ rebuildDOM(): HTMLElement {
+ const newDOM = this.buildDOM();
+ this.dom?.replaceWith(newDOM);
+ this.dom = newDOM;
+ return this.dom;
+ }
+
trans(text: string) {
return this.getContext().translate(text);
}
diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts
index 36371e302..08edb214e 100644
--- a/resources/js/wysiwyg/ui/framework/forms.ts
+++ b/resources/js/wysiwyg/ui/framework/forms.ts
@@ -11,7 +11,7 @@ import {el} from "../../utils/dom";
export interface EditorFormFieldDefinition {
label: string;
name: string;
- type: 'text' | 'select' | 'textarea';
+ type: 'text' | 'select' | 'textarea' | 'checkbox';
}
export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
@@ -19,15 +19,17 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
valuesByLabel: Record
}
+export type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
+
interface EditorFormTabDefinition {
label: string;
- contents: EditorFormFieldDefinition[];
+ contents: EditorFormFields;
}
export interface EditorFormDefinition {
submitText: string;
action: (formData: FormData, context: EditorUiContext) => Promise;
- fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
+ fields: EditorFormFields;
}
export class EditorFormField extends EditorUiElement {
@@ -40,7 +42,12 @@ export class EditorFormField extends EditorUiElement {
setValue(value: string) {
const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;
- input.value = value;
+ if (this.definition.type === 'checkbox') {
+ input.checked = Boolean(value);
+ } else {
+ input.value = value;
+ }
+ input.dispatchEvent(new Event('change'));
}
getName(): string {
@@ -58,6 +65,8 @@ export class EditorFormField extends EditorUiElement {
input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
} else if (this.definition.type === 'textarea') {
input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
+ } else if (this.definition.type === 'checkbox') {
+ input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'});
} else {
input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
}
@@ -155,11 +164,17 @@ export class EditorForm extends EditorContainerUiElement {
export class EditorFormTab extends EditorContainerUiElement {
protected definition: EditorFormTabDefinition;
- protected fields: EditorFormField[];
+ protected fields: EditorUiElement[];
protected id: string;
constructor(definition: EditorFormTabDefinition) {
- const fields = definition.contents.map(fieldDef => new EditorFormField(fieldDef));
+ const fields = definition.contents.map(fieldDef => {
+ if (isUiBuilderDefinition(fieldDef)) {
+ return fieldDef.build();
+ }
+ return new EditorFormField(fieldDef)
+ });
+
super(fields);
this.definition = definition;
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index 2446c1416..9f7694e85 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -649,6 +649,16 @@ textarea.editor-form-field-input {
width: $inputWidth - 40px;
}
}
+.editor-color-field-container {
+ position: relative;
+ input {
+ padding-left: 36px;
+ }
+ .editor-dropdown-menu-container {
+ position: absolute;
+ bottom: 0;
+ }
+}
// Editor theme styles
.editor-theme-bold {