diff --git a/packages/svelte-lexical/src/global.css b/packages/svelte-lexical/src/global.css index 122c1d5d..ccfc3668 100644 --- a/packages/svelte-lexical/src/global.css +++ b/packages/svelte-lexical/src/global.css @@ -464,6 +464,10 @@ i.poll { background-image: url(/images/icons/card-checklist.svg); } +i.columns { + background-image: url(images/icons/3-columns.svg); +} + i.tweet { background-image: url(/images/icons/tweet.svg); } diff --git a/packages/svelte-lexical/src/lib/components/generic/dialog/ModalDialog.svelte b/packages/svelte-lexical/src/lib/components/generic/dialog/ModalDialog.svelte index bb567d30..69dc0c44 100644 --- a/packages/svelte-lexical/src/lib/components/generic/dialog/ModalDialog.svelte +++ b/packages/svelte-lexical/src/lib/components/generic/dialog/ModalDialog.svelte @@ -1,5 +1,6 @@ + + + + Columns Layout + diff --git a/packages/svelte-lexical/src/lib/components/toolbar/dialogs/InsertColumnsDialog.svelte b/packages/svelte-lexical/src/lib/components/toolbar/dialogs/InsertColumnsDialog.svelte new file mode 100644 index 00000000..bd4f96fe --- /dev/null +++ b/packages/svelte-lexical/src/lib/components/toolbar/dialogs/InsertColumnsDialog.svelte @@ -0,0 +1,81 @@ + + + + + + + Insert Columns Layout + + + {#each LAYOUTS as layout} + { + handleClick(layout.label, layout.value); + }}> + {layout.label} + + {/each} + + + + { + $activeEditor.dispatchCommand(INSERT_LAYOUT_COMMAND, currentValue); + close(); + }}> + Insert + + + + + + + diff --git a/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/ColumnLayoutPlugin.svelte b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/ColumnLayoutPlugin.svelte new file mode 100644 index 00000000..c0aa7f21 --- /dev/null +++ b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/ColumnLayoutPlugin.svelte @@ -0,0 +1,204 @@ + diff --git a/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutContainerNode.ts b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutContainerNode.ts new file mode 100644 index 00000000..743136c3 --- /dev/null +++ b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutContainerNode.ts @@ -0,0 +1,129 @@ +import type { + DOMConversionMap, + DOMConversionOutput, + DOMExportOutput, + EditorConfig, + LexicalNode, + NodeKey, + SerializedElementNode, + Spread, +} from 'lexical'; + +import {addClassNamesToElement} from '@lexical/utils'; +import {ElementNode} from 'lexical'; + +export type SerializedLayoutContainerNode = Spread< + { + templateColumns: string; + }, + SerializedElementNode +>; + +function $convertLayoutContainerElement( + domNode: HTMLElement, +): DOMConversionOutput | null { + const styleAttributes = window.getComputedStyle(domNode); + const templateColumns = styleAttributes.getPropertyValue( + 'grid-template-columns', + ); + if (templateColumns) { + const node = $createLayoutContainerNode(templateColumns); + return {node}; + } + return null; +} + +export class LayoutContainerNode extends ElementNode { + __templateColumns: string; + + constructor(templateColumns: string, key?: NodeKey) { + super(key); + this.__templateColumns = templateColumns; + } + + static getType(): string { + return 'layout-container'; + } + + static clone(node: LayoutContainerNode): LayoutContainerNode { + return new LayoutContainerNode(node.__templateColumns, node.__key); + } + + createDOM(config: EditorConfig): HTMLElement { + const dom = document.createElement('div'); + dom.style.gridTemplateColumns = this.__templateColumns; + if (typeof config.theme.layoutContainer === 'string') { + addClassNamesToElement(dom, config.theme.layoutContainer); + } + return dom; + } + + exportDOM(): DOMExportOutput { + const element = document.createElement('div'); + element.style.gridTemplateColumns = this.__templateColumns; + element.setAttribute('data-lexical-layout-container', 'true'); + return {element}; + } + + updateDOM(prevNode: LayoutContainerNode, dom: HTMLElement): boolean { + if (prevNode.__templateColumns !== this.__templateColumns) { + dom.style.gridTemplateColumns = this.__templateColumns; + } + return false; + } + + static importDOM(): DOMConversionMap | null { + return { + div: (domNode: HTMLElement) => { + if (!domNode.hasAttribute('data-lexical-layout-container')) { + return null; + } + return { + conversion: $convertLayoutContainerElement, + priority: 2, + }; + }, + }; + } + + static importJSON(json: SerializedLayoutContainerNode): LayoutContainerNode { + return $createLayoutContainerNode(json.templateColumns); + } + + isShadowRoot(): boolean { + return true; + } + + canBeEmpty(): boolean { + return false; + } + + exportJSON(): SerializedLayoutContainerNode { + return { + ...super.exportJSON(), + templateColumns: this.__templateColumns, + type: 'layout-container', + version: 1, + }; + } + + getTemplateColumns(): string { + return this.getLatest().__templateColumns; + } + + setTemplateColumns(templateColumns: string) { + this.getWritable().__templateColumns = templateColumns; + } +} + +export function $createLayoutContainerNode( + templateColumns: string, +): LayoutContainerNode { + return new LayoutContainerNode(templateColumns); +} + +export function $isLayoutContainerNode( + node: LexicalNode | null | undefined, +): node is LayoutContainerNode { + return node instanceof LayoutContainerNode; +} diff --git a/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutItemNode.ts b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutItemNode.ts new file mode 100644 index 00000000..40f7adf0 --- /dev/null +++ b/packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutItemNode.ts @@ -0,0 +1,75 @@ +import type { + DOMConversionMap, + EditorConfig, + LexicalNode, + SerializedElementNode, + NodeKey, +} from 'lexical'; + +import {ElementNode} from 'lexical'; +import type {LexicalCommand} from 'lexical'; +import {createCommand} from 'lexical'; +import {addClassNamesToElement} from '@lexical/utils'; + +export type SerializedLayoutItemNode = SerializedElementNode; + +export const INSERT_LAYOUT_COMMAND: LexicalCommand = createCommand( + 'INSERT_LAYOUT_COMMAND', +); + +export const UPDATE_LAYOUT_COMMAND: LexicalCommand<{ + template: string; + nodeKey: NodeKey; +}> = createCommand<{template: string; nodeKey: NodeKey}>(); + +export class LayoutItemNode extends ElementNode { + static getType(): string { + return 'layout-item'; + } + + static clone(node: LayoutItemNode): LayoutItemNode { + return new LayoutItemNode(node.__key); + } + + createDOM(config: EditorConfig): HTMLElement { + const dom = document.createElement('div'); + if (typeof config.theme.layoutItem === 'string') { + addClassNamesToElement(dom, config.theme.layoutItem); + } + return dom; + } + + updateDOM(): boolean { + return false; + } + + static importDOM(): DOMConversionMap | null { + return {}; + } + + static importJSON(): LayoutItemNode { + return $createLayoutItemNode(); + } + + isShadowRoot(): boolean { + return true; + } + + exportJSON(): SerializedLayoutItemNode { + return { + ...super.exportJSON(), + type: 'layout-item', + version: 1, + }; + } +} + +export function $createLayoutItemNode(): LayoutItemNode { + return new LayoutItemNode(); +} + +export function $isLayoutItemNode( + node: LexicalNode | null | undefined, +): node is LayoutItemNode { + return node instanceof LayoutItemNode; +} diff --git a/packages/svelte-lexical/src/lib/index.ts b/packages/svelte-lexical/src/lib/index.ts index e1dbccfd..aff49156 100644 --- a/packages/svelte-lexical/src/lib/index.ts +++ b/packages/svelte-lexical/src/lib/index.ts @@ -21,6 +21,7 @@ export {sanitizeUrl, validateUrl} from './core/plugins/link/url.js'; export {default as FloatingLinkEditorPlugin} from './core/plugins/link/FloatingLinkEditorPlugin.svelte'; export {default as CodeHighlightPlugin} from './core/plugins/CodeBlock/CodeHighlightPlugin.svelte'; export {default as CodeActionMenuPlugin} from './core/plugins/CodeBlock/CodeActionMenuPlugin/CodeActionMenuPlugin.svelte'; +export {default as ColumnLayoutPlugin} from './core/plugins/ColumnsLayout/ColumnLayoutPlugin.svelte'; export {default as MarkdownShortcutPlugin} from './core/plugins/MardownShortcut/MarkdownShortcutPlugin.svelte'; export { @@ -58,6 +59,8 @@ export {HashtagNode} from './core/plugins/HashtagNode.js'; export {AutoLinkNode, LinkNode} from '@lexical/link'; export {CodeNode, CodeHighlightNode} from '@lexical/code'; export type {Provider} from '@lexical/yjs'; +export {LayoutContainerNode} from './core/plugins/ColumnsLayout/LayoutContainerNode.js'; +export {LayoutItemNode} from './core/plugins/ColumnsLayout/LayoutItemNode.js'; // toolbar export {default as ToolbarRichText} from './components/richtext/ToolbarRichText.svelte'; @@ -95,10 +98,12 @@ export {default as StrikethroughDropDownItem} from './components/toolbar/MoreSty export {default as SubscriptDropDownItem} from './components/toolbar/MoreStylesDropDown/SubscriptDropDownItem.svelte'; export {default as SuperscriptDropDownItem} from './components/toolbar/MoreStylesDropDown/SuperscriptDropDownItem.svelte'; export {default as ClearFormattingDropDownItem} from './components/toolbar/MoreStylesDropDown/ClearFormattingDropDownItem.svelte'; +export {default as InsertColumnLayoutDropDownItem} from './components/toolbar/InsertDropDown/InsertColumnLayoutDropDownItem.svelte'; // dialogs export {default as InsertImageDialog} from './components/toolbar/dialogs/InsertImageDialog.svelte'; export {default as InsertImageUploadedDialogBody} from './components/toolbar/dialogs/InsertImageUploadedDialogBody.svelte'; export {default as InsertImageUriDialogBody} from './components/toolbar/dialogs/InsertImageUriDialogBody.svelte'; +export {default as InsertColumnsDialog} from './components/toolbar/dialogs/InsertColumnsDialog.svelte'; export {getCommands} from './core/commands.js'; export type {ImagePayload} from './core/plugins/Image/ImageNode.js'; diff --git a/packages/svelte-lexical/src/routes/RichTextComposer.svelte b/packages/svelte-lexical/src/routes/RichTextComposer.svelte index f59f4474..bd10ecf2 100644 --- a/packages/svelte-lexical/src/routes/RichTextComposer.svelte +++ b/packages/svelte-lexical/src/routes/RichTextComposer.svelte @@ -27,6 +27,7 @@ CodeHighlightNode, CodeHighlightPlugin, CodeActionMenuPlugin, + ColumnLayoutPlugin, } from '$lib/index.js'; import { HeadingNode, @@ -35,6 +36,8 @@ ListItemNode, HorizontalRuleNode, ImageNode, + LayoutContainerNode, + LayoutItemNode, } from '$lib/index.js'; import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme.js'; import { @@ -62,6 +65,8 @@ AutoLinkNode, CodeNode, CodeHighlightNode, + LayoutContainerNode, + LayoutItemNode, ], onError: (error: Error) => { throw error; @@ -134,7 +139,7 @@ CHECK_LIST, LINK, ]} /> - + diff --git a/packages/svelte-lexical/src/routes/RichTextToolbar.svelte b/packages/svelte-lexical/src/routes/RichTextToolbar.svelte index 1fa199b1..15fabb8d 100644 --- a/packages/svelte-lexical/src/routes/RichTextToolbar.svelte +++ b/packages/svelte-lexical/src/routes/RichTextToolbar.svelte @@ -26,8 +26,11 @@ import {InsertImageDialog} from '../lib/index.js'; import {InsertHRDropDownItem} from '../lib/index.js'; import {InsertImageDropDownItem} from '../lib/index.js'; + import {InsertColumnLayoutDropDownItem} from '../lib/index.js'; + import {InsertColumnsDialog} from '../lib/index.js'; let imageDialog: InsertImageDialog; + let columnsDialog: InsertColumnsDialog; @@ -61,9 +64,11 @@ imageDialog.open()} /> + columnsDialog.open()} /> + diff --git a/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.css b/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.css index 4e5b551e..82ae6754 100644 --- a/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.css +++ b/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.css @@ -6,6 +6,15 @@ * * */ +.PlaygroundEditorTheme__layoutContainer { + display: grid; + gap: 10px; + margin: 10px 0; +} +.PlaygroundEditorTheme__layoutItem { + border: 1px dashed #ddd; + padding: 8px 16px; +} .PlaygroundEditorTheme__ltr { text-align: left; } diff --git a/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.js b/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.js index ffc3adf7..e781fb4b 100644 --- a/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.js +++ b/packages/svelte-lexical/src/routes/themes/PlaygroundEditorTheme.js @@ -9,6 +9,8 @@ import './PlaygroundEditorTheme.css'; const theme = { + layoutContainer: 'PlaygroundEditorTheme__layoutContainer', + layoutItem: 'PlaygroundEditorTheme__layoutItem', blockCursor: 'PlaygroundEditorTheme__blockCursor', characterLimit: 'PlaygroundEditorTheme__characterLimit', code: 'PlaygroundEditorTheme__code',