fix: Fixed issue with checklist items in Super notes not being correctly exported to HTML

This commit is contained in:
Aman Harwara
2023-10-29 20:05:40 +05:30
parent cace6522f6
commit 198efdc41c
9 changed files with 239 additions and 193 deletions

View File

@@ -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

View File

@@ -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())
},
},
]

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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`)
},

View File

@@ -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',
}
}
}

View File

@@ -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

View File

@@ -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';