diff --git a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx index bab70ab1c..81b39e7d6 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/BlocksEditor.tsx @@ -27,7 +27,7 @@ import { RemoveBrokenTablesPlugin } from './Plugins/TablePlugin' import TableActionMenuPlugin from './Plugins/TableCellActionMenuPlugin' import ToolbarPlugin from './Plugins/ToolbarPlugin/ToolbarPlugin' import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery' -import { CheckListPlugin } from './Plugins/CheckListPlugin' +import { CheckListPlugin } from './Plugins/List/CheckListPlugin' type BlocksEditorProps = { onChange?: (value: string, preview: string) => void diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/AllNodes.ts b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/AllNodes.ts index 738a82b4c..d6ed373d2 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/AllNodes.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Nodes/AllNodes.ts @@ -16,8 +16,10 @@ import { FileNode } from '../../Plugins/EncryptedFilePlugin/Nodes/FileNode' import { BubbleNode } from '../../Plugins/ItemBubblePlugin/Nodes/BubbleNode' import { RemoteImageNode } from '../../Plugins/RemoteImagePlugin/RemoteImageNode' import { InlineFileNode } from '../../Plugins/InlineFilePlugin/InlineFileNode' +import { CreateEditorArgs } from 'lexical' +import { ListHTMLExportNode } from '../../Plugins/List/ListHTMLExportNode' -export const BlockEditorNodes = [ +const CommonNodes = [ AutoLinkNode, CodeHighlightNode, CodeNode, @@ -29,7 +31,6 @@ export const BlockEditorNodes = [ HorizontalRuleNode, LinkNode, ListItemNode, - ListNode, MarkNode, OverflowNode, QuoteNode, @@ -43,3 +44,15 @@ export const BlockEditorNodes = [ RemoteImageNode, InlineFileNode, ] + +export const BlockEditorNodes = [...CommonNodes, ListNode] +export const HTMLExportNodes: CreateEditorArgs['nodes'] = [ + ...CommonNodes, + ListHTMLExportNode, + { + replace: ListNode, + with(node) { + return new ListHTMLExportNode(node.getListType(), node.getStart()) + }, + }, +] diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss index e42d99763..2cb5e8152 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/editor.scss @@ -1,3 +1,5 @@ +@import 'lists'; + .Lexical__ltr { text-align: left; } @@ -163,70 +165,6 @@ box-shadow: 0 0 5px rgba(0, 0, 0, 0.4); border-radius: 3px; } -.Lexical__tableAddColumns { - position: absolute; - top: 0; - width: 20px; - background-color: #eee; - height: 100%; - right: 0; - animation: table-controls 0.2s ease; - border: 0; - cursor: pointer; -} -.Lexical__tableAddColumns:after { - background-image: url(#{$blocks-editor-icons-path}/plus.svg); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - display: block; - content: ' '; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.4; -} -.Lexical__tableAddColumns:hover { - background-color: #c9dbf0; -} -.Lexical__tableAddRows { - position: absolute; - bottom: -25px; - width: calc(100% - 25px); - background-color: #eee; - height: 20px; - left: 0; - animation: table-controls 0.2s ease; - border: 0; - cursor: pointer; -} -.Lexical__tableAddRows:after { - background-image: url(#{$blocks-editor-icons-path}/plus.svg); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - display: block; - content: ' '; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 0.4; -} -.Lexical__tableAddRows:hover { - background-color: #c9dbf0; -} -@keyframes table-controls { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} .Lexical__tableCellResizeRuler { display: block; position: absolute; @@ -261,125 +199,6 @@ display: inline; background-color: #ffbbbb !important; } -:root { - --lexical-ordered-list-left-margin: 16px; -} -.monospace-font { - --lexical-ordered-list-left-margin: 42px; -} -@for $i from 1 through 5 { - .Lexical__ol#{$i} { - padding: 0; - margin: 0; - margin-left: var(--lexical-ordered-list-left-margin); - list-style-position: outside; - - &.Lexical__rtl { - margin-left: 0; - margin-right: var(--lexical-ordered-list-left-margin); - } - } -} -.Lexical__ol2 { - list-style-type: upper-alpha; -} -.Lexical__ol3 { - list-style-type: lower-alpha; -} -.Lexical__ol4 { - list-style-type: upper-roman; -} -.Lexical__ol5 { - list-style-type: lower-roman; -} -.Lexical__ul { - padding: 0; - margin: 0; - margin-left: 16px; - list-style-position: outside; - - &.Lexical__rtl { - margin-left: 0; - margin-right: 16px; - } -} -.Lexical__checkList { - margin-left: 0; - .Lexical__nestedListItem & { - margin-left: 16px; - } -} -.Lexical__listItem { - margin: 0 0px; -} -.Lexical__listItemChecked, -.Lexical__listItemUnchecked { - position: relative; - padding-left: 24px; - padding-right: 24px; - list-style-type: none; - outline: none; - vertical-align: middle; -} -.Lexical__listItemChecked { - text-decoration: line-through; - opacity: 0.4; -} -.Lexical__listItemUnchecked:before, -.Lexical__listItemChecked:before { - content: ''; - width: 16px; - height: 16px; - left: 0; - top: 7px; - cursor: pointer; - background-size: cover; - position: absolute; -} -.Lexical__listItemUnchecked[dir='rtl']:before, -.Lexical__listItemChecked[dir='rtl']:before { - left: auto; - right: 0; -} -.Lexical__listItemUnchecked:focus:before, -.Lexical__listItemChecked:focus:before { - box-shadow: 0 0 0 2px #a6cdfe; - border-radius: 2px; -} -.Lexical__listItemUnchecked:before { - border: 1px solid #999; - border-radius: 2px; -} -.Lexical__listItemChecked:before { - border: 1px solid var(--sn-stylekit-info-color); - border-radius: 2px; - background-color: var(--sn-stylekit-info-color); - background-repeat: no-repeat; -} -.Lexical__listItemChecked:after { - content: ''; - cursor: pointer; - border-color: var(--sn-stylekit-info-contrast-color); - border-style: solid; - position: absolute; - display: block; - top: 9px; - width: 5px; - left: 6px; - height: 10px; - transform: rotate(45deg); - border-width: 0 2px 2px 0; -} -.Lexical__nestedListItem { - list-style-type: none; - &.Lexical__listItemUnchecked { - padding-left: 0; - } -} -.Lexical__nestedListItem:before, -.Lexical__nestedListItem:after { - display: none; -} .Lexical__tokenComment { color: slategray; } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/lists.scss b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/lists.scss new file mode 100644 index 000000000..88d798989 --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Lexical/Theme/lists.scss @@ -0,0 +1,119 @@ +:root { + --lexical-ordered-list-left-margin: 16px; +} +.monospace-font { + --lexical-ordered-list-left-margin: 42px; +} +@for $i from 1 through 5 { + .Lexical__ol#{$i} { + padding: 0; + margin: 0; + margin-left: var(--lexical-ordered-list-left-margin); + list-style-position: outside; + + &.Lexical__rtl { + margin-left: 0; + margin-right: var(--lexical-ordered-list-left-margin); + } + } +} +.Lexical__ol2 { + list-style-type: upper-alpha; +} +.Lexical__ol3 { + list-style-type: lower-alpha; +} +.Lexical__ol4 { + list-style-type: upper-roman; +} +.Lexical__ol5 { + list-style-type: lower-roman; +} +.Lexical__ul { + padding: 0; + margin: 0; + margin-left: 16px; + list-style-position: outside; + + &.Lexical__rtl { + margin-left: 0; + margin-right: 16px; + } +} +.Lexical__checkList { + margin-left: 0; + .Lexical__nestedListItem & { + margin-left: 16px; + } +} +.Lexical__listItem { + margin: 0 0px; +} +.Lexical__listItemChecked, +.Lexical__listItemUnchecked { + position: relative; + padding-left: 24px; + padding-right: 24px; + list-style-type: none; + outline: none; + vertical-align: middle; +} +.Lexical__listItemChecked { + text-decoration: line-through; + opacity: 0.4; +} +.Lexical__listItemUnchecked:before, +.Lexical__listItemChecked:before { + content: ''; + width: 16px; + height: 16px; + left: 0; + top: 7px; + cursor: pointer; + background-size: cover; + position: absolute; +} +.Lexical__listItemUnchecked[dir='rtl']:before, +.Lexical__listItemChecked[dir='rtl']:before { + left: auto; + right: 0; +} +.Lexical__listItemUnchecked:focus:before, +.Lexical__listItemChecked:focus:before { + box-shadow: 0 0 0 2px #a6cdfe; + border-radius: 2px; +} +.Lexical__listItemUnchecked:before { + border: 1px solid #999; + border-radius: 2px; +} +.Lexical__listItemChecked:before { + border: 1px solid var(--sn-stylekit-info-color); + border-radius: 2px; + background-color: var(--sn-stylekit-info-color); + background-repeat: no-repeat; +} +.Lexical__listItemChecked:after { + content: ''; + cursor: pointer; + border-color: var(--sn-stylekit-info-contrast-color); + border-style: solid; + position: absolute; + display: block; + top: 9px; + width: 5px; + left: 6px; + height: 10px; + transform: rotate(45deg); + border-width: 0 2px 2px 0; +} +.Lexical__nestedListItem { + list-style-type: none; + &.Lexical__listItemUnchecked { + padding-left: 0; + } +} +.Lexical__nestedListItem:before, +.Lexical__nestedListItem:after { + display: none; +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts index 0722f5351..d4cdce2fa 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/ExportPlugin/ExportPlugin.ts @@ -13,6 +13,38 @@ import { useCallback, useEffect, useRef } from 'react' import { useCommandService } from '@/Components/CommandProvider' import { HeadlessSuperConverter } from '../../Tools/HeadlessSuperConverter' +// @ts-expect-error Using inline loaders to load CSS as string +import superEditorCSS from '!css-loader!sass-loader!../../Lexical/Theme/editor.scss' +// @ts-expect-error Using inline loaders to load CSS as string +import snColorsCSS from '!css-loader!sass-loader!@standardnotes/styles/src/Styles/_colors.scss' + +const html = (title: string, content: string) => ` + + + + + ${title} + + + + ${content} + + +` + export const ExportPlugin = () => { const application = useApplication() const [editor] = useLexicalComposerContext() @@ -58,7 +90,10 @@ export const ExportPlugin = () => { const exportHtml = useCallback( (title: string) => { - const content = converter.current.convertSuperStringToOtherFormat(JSON.stringify(editor.getEditorState()), 'html') + const content = html( + title, + converter.current.convertSuperStringToOtherFormat(JSON.stringify(editor.getEditorState()), 'html'), + ) const blob = new Blob([content], { type: 'text/html' }) downloadData(blob, `${sanitizeFileName(title)}.html`) }, diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/CheckListPlugin.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/List/CheckListPlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/SuperEditor/Plugins/CheckListPlugin.tsx rename to packages/web/src/javascripts/Components/SuperEditor/Plugins/List/CheckListPlugin.tsx diff --git a/packages/web/src/javascripts/Components/SuperEditor/Plugins/List/ListHTMLExportNode.tsx b/packages/web/src/javascripts/Components/SuperEditor/Plugins/List/ListHTMLExportNode.tsx new file mode 100644 index 000000000..382cc7c64 --- /dev/null +++ b/packages/web/src/javascripts/Components/SuperEditor/Plugins/List/ListHTMLExportNode.tsx @@ -0,0 +1,38 @@ +import { ListNode, SerializedListNode } from '@lexical/list' +import { DOMExportOutput, LexicalEditor, Spread } from 'lexical' + +export type SerializedListHTMLExportNode = Spread< + { + type: 'list-html-export' + }, + SerializedListNode +> + +export class ListHTMLExportNode extends ListNode { + static getType(): string { + return 'list-html-export' + } + + static clone(node: ListNode): ListHTMLExportNode { + return new ListHTMLExportNode(node.getListType(), node.getStart(), node.getKey()) + } + + static importJSON(serializedNode: SerializedListNode): ListNode { + return super.importJSON(serializedNode) + } + + exportDOM(editor: LexicalEditor): DOMExportOutput { + const { element } = super.exportDOM(editor) + if (this.getListType() === 'check' && element instanceof HTMLElement) { + element.classList.add('Lexical__checkList') + } + return { element } + } + + exportJSON(): SerializedListHTMLExportNode { + return { + ...super.exportJSON(), + type: 'list-html-export', + } + } +} diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx index b375e845b..330db074f 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx @@ -11,12 +11,13 @@ import { ParagraphNode, } from 'lexical' import BlocksEditorTheme from '../Lexical/Theme/Theme' -import { BlockEditorNodes } from '../Lexical/Nodes/AllNodes' +import { BlockEditorNodes, HTMLExportNodes } from '../Lexical/Nodes/AllNodes' import { MarkdownTransformers } from '../MarkdownTransformers' import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html' export class HeadlessSuperConverter implements SuperConverterServiceInterface { private editor: LexicalEditor + private htmlExportEditor: LexicalEditor constructor() { this.editor = createHeadlessEditor({ @@ -26,6 +27,13 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { onError: (error: Error) => console.error(error), nodes: [...BlockEditorNodes], }) + this.htmlExportEditor = createHeadlessEditor({ + namespace: 'BlocksEditor', + theme: BlocksEditorTheme, + editable: false, + onError: (error: Error) => console.error(error), + nodes: HTMLExportNodes, + }) } isValidSuperString(superString: string): boolean { @@ -42,6 +50,25 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { return superString } + if (toFormat === 'html') { + this.htmlExportEditor.setEditorState(this.htmlExportEditor.parseEditorState(superString)) + + let content: string | undefined + + this.htmlExportEditor.update( + () => { + content = $generateHtmlFromNodes(this.htmlExportEditor) + }, + { discrete: true }, + ) + + if (typeof content !== 'string') { + throw new Error('Could not export note') + } + + return content + } + this.editor.setEditorState(this.editor.parseEditorState(superString)) let content: string | undefined @@ -60,9 +87,6 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { content = $convertToMarkdownString(MarkdownTransformers) break } - case 'html': - content = $generateHtmlFromNodes(this.editor) - break case 'json': default: content = superString diff --git a/packages/web/src/stylesheets/index.css.scss b/packages/web/src/stylesheets/index.css.scss index cae4551d2..41012126d 100644 --- a/packages/web/src/stylesheets/index.css.scss +++ b/packages/web/src/stylesheets/index.css.scss @@ -1,5 +1,3 @@ -$blocks-editor-icons-path: '../javascripts/Components/SuperEditor/Lexical/Icons'; - @import '../../../styles/src/Styles/_colors.scss'; @import '../../../styles/src/Styles/_panels.scss'; @import '../../../styles/src/Styles/_scrollbar.scss';