forked from umaranis/svelte-lexical
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Richard Scheglmann
committed
Sep 14, 2024
1 parent
aaf0fed
commit 98e7b54
Showing
4 changed files
with
412 additions
and
0 deletions.
There are no files selected for viewing
205 changes: 205 additions & 0 deletions
205
packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/ColumnLayoutPlugin.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
<script lang="ts"> | ||
import type {ElementNode, LexicalNode} from 'lexical'; | ||
import {getEditor} from '../../composerContext.js'; | ||
import { | ||
$findMatchingParent as findMatchingParent, | ||
$insertNodeToNearestRoot as insertNodeToNearestRoot, | ||
mergeRegister, | ||
} from '@lexical/utils'; | ||
import { | ||
$createParagraphNode as createParagraphNode, | ||
$getNodeByKey as getNodeByKey, | ||
$getSelection as getSelection, | ||
$isRangeSelection as isRangeSelection, | ||
COMMAND_PRIORITY_EDITOR, | ||
COMMAND_PRIORITY_LOW, | ||
KEY_ARROW_DOWN_COMMAND, | ||
KEY_ARROW_LEFT_COMMAND, | ||
KEY_ARROW_RIGHT_COMMAND, | ||
KEY_ARROW_UP_COMMAND, | ||
} from 'lexical'; | ||
import {onMount} from 'svelte'; | ||
import { | ||
$createLayoutContainerNode as createLayoutContainerNode, | ||
$isLayoutContainerNode as isLayoutContainerNode, | ||
LayoutContainerNode, | ||
} from './LayoutContainerNode.js'; | ||
import { | ||
$createLayoutItemNode as createLayoutItemNode, | ||
$isLayoutItemNode as isLayoutItemNode, | ||
LayoutItemNode, | ||
INSERT_LAYOUT_COMMAND, | ||
UPDATE_LAYOUT_COMMAND, | ||
} from './LayoutItemNode.js'; | ||
const editor = getEditor(); | ||
onMount(() => { | ||
if (!editor.hasNodes([LayoutContainerNode, LayoutItemNode])) { | ||
throw new Error( | ||
'LayoutPlugin: LayoutContainerNode, or LayoutItemNode not registered on editor', | ||
); | ||
} | ||
const $onEscape = (before: boolean) => { | ||
const selection = getSelection(); | ||
if ( | ||
isRangeSelection(selection) && | ||
selection.isCollapsed() && | ||
selection.anchor.offset === 0 | ||
) { | ||
const container = findMatchingParent( | ||
selection.anchor.getNode(), | ||
isLayoutContainerNode, | ||
); | ||
if (isLayoutContainerNode(container)) { | ||
const parent = container.getParent<ElementNode>(); | ||
const child = | ||
parent && | ||
(before | ||
? parent.getFirstChild<LexicalNode>() | ||
: parent?.getLastChild<LexicalNode>()); | ||
const descendant = before | ||
? container.getFirstDescendant<LexicalNode>()?.getKey() | ||
: container.getLastDescendant<LexicalNode>()?.getKey(); | ||
if ( | ||
parent !== null && | ||
child === container && | ||
selection.anchor.key === descendant | ||
) { | ||
if (before) { | ||
container.insertBefore(createParagraphNode()); | ||
} else { | ||
container.insertAfter(createParagraphNode()); | ||
} | ||
} | ||
} | ||
} | ||
return false; | ||
}; | ||
return mergeRegister( | ||
// When layout is the last child pressing down/right arrow will insert paragraph | ||
// below it to allow adding more content. It's similar what $insertBlockNode | ||
// (mainly for decorators), except it'll always be possible to continue adding | ||
// new content even if trailing paragraph is accidentally deleted | ||
editor.registerCommand( | ||
KEY_ARROW_DOWN_COMMAND, | ||
() => $onEscape(false), | ||
COMMAND_PRIORITY_LOW, | ||
), | ||
editor.registerCommand( | ||
KEY_ARROW_RIGHT_COMMAND, | ||
() => $onEscape(false), | ||
COMMAND_PRIORITY_LOW, | ||
), | ||
// When layout is the first child pressing up/left arrow will insert paragraph | ||
// above it to allow adding more content. It's similar what $insertBlockNode | ||
// (mainly for decorators), except it'll always be possible to continue adding | ||
// new content even if leading paragraph is accidentally deleted | ||
editor.registerCommand( | ||
KEY_ARROW_UP_COMMAND, | ||
() => $onEscape(true), | ||
COMMAND_PRIORITY_LOW, | ||
), | ||
editor.registerCommand( | ||
KEY_ARROW_LEFT_COMMAND, | ||
() => $onEscape(true), | ||
COMMAND_PRIORITY_LOW, | ||
), | ||
editor.registerCommand( | ||
INSERT_LAYOUT_COMMAND, | ||
(template: string) => { | ||
editor.update(() => { | ||
console.log(template); | ||
const container = createLayoutContainerNode(template); | ||
const itemsCount = getItemsCountFromTemplate(template); | ||
for (let i = 0; i < itemsCount; i++) { | ||
container.append( | ||
createLayoutItemNode().append(createParagraphNode()), | ||
); | ||
} | ||
insertNodeToNearestRoot(container); | ||
container.selectStart(); | ||
}); | ||
return true; | ||
}, | ||
COMMAND_PRIORITY_EDITOR, | ||
), | ||
editor.registerCommand( | ||
UPDATE_LAYOUT_COMMAND, | ||
({template, nodeKey}) => { | ||
editor.update(() => { | ||
const container = getNodeByKey<LexicalNode>(nodeKey); | ||
if (!isLayoutContainerNode(container)) { | ||
return; | ||
} | ||
const itemsCount = getItemsCountFromTemplate(template); | ||
const prevItemsCount = getItemsCountFromTemplate( | ||
container.getTemplateColumns(), | ||
); | ||
// Add or remove extra columns if new template does not match existing one | ||
if (itemsCount > prevItemsCount) { | ||
for (let i = prevItemsCount; i < itemsCount; i++) { | ||
container.append( | ||
createLayoutItemNode().append(createParagraphNode()), | ||
); | ||
} | ||
} else if (itemsCount < prevItemsCount) { | ||
for (let i = prevItemsCount - 1; i >= itemsCount; i--) { | ||
const layoutItem = container.getChildAtIndex<LexicalNode>(i); | ||
if (isLayoutItemNode(layoutItem)) { | ||
layoutItem.remove(); | ||
} | ||
} | ||
} | ||
container.setTemplateColumns(template); | ||
}); | ||
return true; | ||
}, | ||
COMMAND_PRIORITY_EDITOR, | ||
), | ||
// Structure enforcing transformers for each node type. In case nesting structure is not | ||
// "Container > Item" it'll unwrap nodes and convert it back | ||
// to regular content. | ||
editor.registerNodeTransform(LayoutItemNode, (node) => { | ||
const parent = node.getParent<ElementNode>(); | ||
if (!isLayoutContainerNode(parent)) { | ||
const children = node.getChildren<LexicalNode>(); | ||
for (const child of children) { | ||
node.insertBefore(child); | ||
} | ||
node.remove(); | ||
} | ||
}), | ||
editor.registerNodeTransform(LayoutContainerNode, (node) => { | ||
const children = node.getChildren<LexicalNode>(); | ||
if (!children.every(isLayoutItemNode)) { | ||
for (const child of children) { | ||
node.insertBefore(child); | ||
} | ||
node.remove(); | ||
} | ||
}), | ||
); | ||
}); | ||
function getItemsCountFromTemplate(template: string): number { | ||
return template.trim().split(/\s+/).length; | ||
} | ||
</script> |
129 changes: 129 additions & 0 deletions
129
packages/svelte-lexical/src/lib/core/plugins/ColumnsLayout/LayoutContainerNode.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Oops, something went wrong.