fix: Fixed issue with checklist items in Super notes not being correctly exported to HTML
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${title}</title>
|
||||
<style>
|
||||
${snColorsCSS.toString()}
|
||||
${superEditorCSS.toString()}
|
||||
.Lexical__listItemUnchecked, .Lexical__listItemChecked {
|
||||
min-height: 18px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.Lexical__listItemUnchecked:before, .Lexical__listItemChecked:before {
|
||||
top: 0px;
|
||||
}
|
||||
.Lexical__listItemChecked:after {
|
||||
top: 1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
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`)
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user