refactored and linted

This commit is contained in:
tonyrewin 2022-10-21 13:17:38 +03:00
parent 92906931da
commit 5d5706b5d7
22 changed files with 253 additions and 267 deletions

View File

@ -12,8 +12,8 @@ export default () => {
<Match when={store.error.id === 'invalid_config'}> <Match when={store.error.id === 'invalid_config'}>
<InvalidState title='Invalid Config' /> <InvalidState title='Invalid Config' />
</Match> </Match>
<Match when={store.error.id === 'invalid_file'}> <Match when={store.error.id === 'invalid_draft'}>
<InvalidState title='Invalid File' /> <InvalidState title='Invalid Draft' />
</Match> </Match>
</Switch> </Switch>
) )

View File

@ -1,14 +1,14 @@
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js' import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { unwrap } from 'solid-js/store' import { unwrap } from 'solid-js/store'
import { undo, redo } from 'prosemirror-history' import { undo, redo } from 'prosemirror-history'
import { File, useState } from '../store/context' import { Draft, useState } from '../store/context'
import { mod } from '../env' import { mod } from '../env'
import * as remote from '../remote' import * as remote from '../remote'
import { isEmpty } from '../prosemirror/helpers' import { isEmpty } from '../prosemirror/helpers'
import { Styled } from './Layout' import type { Styled } from './Layout'
import '../styles/Sidebar.scss' import '../styles/Sidebar.scss'
const Off = ({ children }: Styled) => <div class='sidebar-off'>{children}</div> const Off = (props) => <div class='sidebar-off'>{props.children}</div>
const Label = (props: Styled) => <h3 class='sidebar-label'>{props.children}</h3> const Label = (props: Styled) => <h3 class='sidebar-label'>{props.children}</h3>
@ -16,8 +16,8 @@ const Link = (
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string } props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
) => ( ) => (
<button <button
class={`sidebar-link${props.className ? ` ${props.className}` : ''}`} class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
style={{ marginBottom: props.withMargin ? '10px' : '' }} style={{ "margin-bottom": props.withMargin ? '10px' : '' }}
onClick={props.onClick} onClick={props.onClick}
disabled={props.disabled} disabled={props.disabled}
title={props.title} title={props.title}
@ -34,10 +34,10 @@ export const Sidebar = () => {
document.body.classList.toggle('dark') document.body.classList.toggle('dark')
ctrl.updateConfig({ theme: document.body.className }) ctrl.updateConfig({ theme: document.body.className })
} }
const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start') const collabText = () => (store.collab?.started ? 'Stop' : (store.collab?.error ? 'Restart 🚨' : 'Start'))
const editorView = () => unwrap(store.editorView) const editorView = () => unwrap(store.editorView)
const onToggleMarkdown = () => ctrl.toggleMarkdown() const onToggleMarkdown = () => ctrl.toggleMarkdown()
const onOpenFile = (file: File) => ctrl.openFile(unwrap(file)) const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0 const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
const onUndo = () => undo(editorView().state, editorView().dispatch) const onUndo = () => undo(editorView().state, editorView().dispatch)
const onRedo = () => redo(editorView().state, editorView().dispatch) const onRedo = () => redo(editorView().state, editorView().dispatch)
@ -56,7 +56,8 @@ export const Sidebar = () => {
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state) store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
} }
const FileLink = (p: { file: File }) => { // eslint-disable-next-line sonarjs/cognitive-complexity
const DraftLink = (p: { draft: Draft }) => {
const length = 100 const length = 100
let content = '' let content = ''
const getContent = (node: any) => { const getContent = (node: any) => {
@ -65,7 +66,7 @@ export const Sidebar = () => {
} }
if (content.length > length) { if (content.length > length) {
content = content.substring(0, length) + '...' content = content.slice(0, Math.max(0, length)) + '...'
return content return content
} }
@ -83,46 +84,47 @@ export const Sidebar = () => {
} }
const text = () => const text = () =>
p.file.path ? p.file.path.substring(p.file.path.length - length) : getContent(p.file.text?.doc) p.draft.path ? p.draft.path.slice(Math.max(0, p.draft.path.length - length)) : getContent(p.draft.text?.doc)
return ( return (
<Link className='file' onClick={() => onOpenFile(p.file)} data-testid='open'> // eslint-disable-next-line solid/no-react-specific-props
{text()} {p.file.path && '📎'} <Link className='draft' onClick={() => onOpenDraft(p.draft)} data-testid='open'>
{text()} {p.draft.path && '📎'}
</Link> </Link>
) )
} }
const Keys = ({ keys }: { keys: string[] }) => ( const Keys = (props) => (
<span> <span>
{keys.map((k) => ( <For each={props.keys}>{(k: Element) => (
<i>{k}</i> <i>{k}</i>
))} )}</For>
</span> </span>
) )
createEffect(() => { createEffect(() => {
setLastAction(undefined) setLastAction()
}, store.lastModified) }, store.lastModified)
createEffect(() => { createEffect(() => {
if (!lastAction()) return if (!lastAction()) return
const id = setTimeout(() => { const id = setTimeout(() => {
setLastAction(undefined) setLastAction()
}, 1000) }, 1000)
onCleanup(() => clearTimeout(id)) onCleanup(() => clearTimeout(id))
}) })
return ( return (
<div className={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}> <div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
<span className='sidebar-opener' onClick={toggleSidebar}>Советы и&nbsp;предложения</span> <span class='sidebar-opener' onClick={toggleSidebar}>Советы и&nbsp;предложения</span>
<Off onClick={() => editorView().focus()}> <Off onClick={() => editorView().focus()}>
<div className='sidebar-closer' onClick={toggleSidebar}/> <div class='sidebar-closer' onClick={toggleSidebar}/>
<Show when={true}> <Show when={true}>
<div> <div>
{store.path && ( {store.path && (
<Label> <Label>
<i>({store.path.substring(store.path.length - 24)})</i> <i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
</Label> </Label>
)} )}
<Link> <Link>
@ -142,26 +144,26 @@ export const Sidebar = () => {
</div> </div>
<Link <Link
onClick={onDiscard} onClick={onDiscard}
disabled={!store.path && store.files.length === 0 && isEmpty(store.text)} disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
data-testid='discard' data-testid='discard'
> >
{store.path ? 'Close' : store.files.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '} {store.path ? 'Close' : (store.drafts.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear')}{' '}
<Keys keys={[mod, 'w']} /> <Keys keys={[mod, 'w']} />
</Link> </Link>
<Link onClick={onUndo}> <Link onClick={onUndo}>
Undo <Keys keys={[mod, 'z']} /> Undo <Keys keys={[mod, 'z']} />
</Link> </Link>
<Link onClick={onRedo}> <Link onClick={onRedo}>
Redo <Keys keys={[mod, ...['Shift', 'z']]} /> Redo <Keys keys={[mod, 'Shift', 'z']} />
</Link> </Link>
<Link onClick={onToggleMarkdown} data-testid='markdown'> <Link onClick={onToggleMarkdown} data-testid='markdown'>
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} /> Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
</Link> </Link>
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link> <Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
<Show when={store.files.length > 0}> <Show when={store.drafts.length > 0}>
<h4>Drafts:</h4> <h4>Drafts:</h4>
<p> <p>
<For each={store.files}>{(file) => <FileLink file={file} />}</For> <For each={store.drafts}>{(draft) => <DraftLink draft={draft} />}</For>
</p> </p>
</Show> </Show>
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}> <Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>

View File

@ -6,7 +6,7 @@ import { history } from 'prosemirror-history'
import { dropCursor } from 'prosemirror-dropcursor' import { dropCursor } from 'prosemirror-dropcursor'
import { buildKeymap } from 'prosemirror-example-setup' import { buildKeymap } from 'prosemirror-example-setup'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const plainSchema = new Schema({ const plainSchema = new Schema({
nodes: { nodes: {

View File

@ -1,12 +1,12 @@
import { inputRules } from 'prosemirror-inputrules' import { inputRules } from 'prosemirror-inputrules'
import { Mark, MarkType } from 'prosemirror-model' import type { Mark, MarkType } from 'prosemirror-model'
import { EditorState, Transaction } from 'prosemirror-state' import type { EditorState, Transaction } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { markInputRule } from './mark-input-rule' import { markInputRule } from './mark-input-rule'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const blank = '\xa0' const blank = '\u00A0'
const onArrow = const onArrow =
(dir: 'left' | 'right') => (dir: 'left' | 'right') =>
@ -36,7 +36,7 @@ const codeKeymap = {
ArrowRight: onArrow('right') ArrowRight: onArrow('right')
} }
const codeRule = (nodeType: MarkType) => markInputRule(/(?:`)([^`]+)(?:`)$/, nodeType) const codeRule = (nodeType: MarkType) => markInputRule(/`([^`]+)`$/, nodeType)
export default (): ProseMirrorExtension => ({ export default (): ProseMirrorExtension => ({
plugins: (prev, schema) => [ plugins: (prev, schema) => [

View File

@ -1,8 +1,10 @@
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror' import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
import { YOptions } from '../../store/context' import type { YOptions } from '../../store/context'
export const cursorBuilder = (user: any): HTMLElement => { interface YUser { background: string, foreground: string, name: string }
export const cursorBuilder = (user: YUser): HTMLElement => {
const cursor = document.createElement('span') const cursor = document.createElement('span')
cursor.classList.add('ProseMirror-yjs-cursor') cursor.classList.add('ProseMirror-yjs-cursor')
cursor.setAttribute('style', `border-color: ${user.background}`) cursor.setAttribute('style', `border-color: ${user.background}`)
@ -19,7 +21,6 @@ export default (y: YOptions): ProseMirrorExtension => ({
? [ ? [
...prev, ...prev,
ySyncPlugin(y.type), ySyncPlugin(y.type),
// @ts-ignore
yCursorPlugin(y.provider.awareness, { cursorBuilder }), yCursorPlugin(y.provider.awareness, { cursorBuilder }),
yUndoPlugin() yUndoPlugin()
] ]

View File

@ -1,6 +1,6 @@
import { Plugin, NodeSelection } from 'prosemirror-state' import { Plugin, NodeSelection } from 'prosemirror-state'
import { DecorationSet, Decoration } from 'prosemirror-view' import { DecorationSet, Decoration } from 'prosemirror-view'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const handleIcon = ` const handleIcon = `
<svg viewBox="0 0 10 10" height="14" width="14"> <svg viewBox="0 0 10 10" height="14" width="14">
@ -22,11 +22,9 @@ const handlePlugin = new Plugin({
decorations(state) { decorations(state) {
const decos = [] const decos = []
state.doc.forEach((node, pos) => { state.doc.forEach((node, pos) => {
decos.push(Decoration.widget(pos + 1, createDragHandle))
decos.push( decos.push(
Decoration.node(pos, pos + node.nodeSize, { Decoration.widget(pos + 1, createDragHandle),
class: 'draggable' Decoration.node(pos, pos + node.nodeSize, { class: 'draggable' })
})
) )
}) })

View File

@ -1,21 +1,22 @@
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { Node, Schema } from 'prosemirror-model' import type { Node, Schema } from 'prosemirror-model'
import { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { ProseMirrorExtension } from '../helpers' import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
import type OrderedMap from 'orderedmap'
const REGEX = /^!\[([^[\]]*?)\]\((.+?)\)\s+/ const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
const MAX_MATCH = 500 const MAX_MATCH = 500
const isUrl = (str: string) => { const isUrl = (str: string) => {
try { try {
const url = new URL(str) const url = new URL(str)
return url.protocol === 'http:' || url.protocol === 'https:' return url.protocol === 'http:' || url.protocol === 'https:'
} catch (_) { } catch {
return false return false
} }
} }
const isBlank = (text: string) => text === ' ' || text === '\xa0' const isBlank = (text: string) => text === ' ' || text === '\u00A0'
const imageInput = (schema: Schema, path?: string) => const imageInput = (schema: Schema, path?: string) =>
new Plugin({ new Plugin({
@ -29,7 +30,7 @@ const imageInput = (schema: Schema, path?: string) =>
Math.max(0, $from.parentOffset - MAX_MATCH), Math.max(0, $from.parentOffset - MAX_MATCH),
$from.parentOffset, $from.parentOffset,
null, null,
'\ufffc' '\uFFFC'
) + text ) + text
const match = REGEX.exec(textBefore) const match = REGEX.exec(textBefore)
@ -65,11 +66,11 @@ const imageSchema = {
parseDOM: [ parseDOM: [
{ {
tag: 'img[src]', tag: 'img[src]',
getAttrs: (dom: Element) => ({ getAttrs: (dom: HTMLElement) => ({
src: dom.getAttribute('src'), src: dom.getAttribute('src'),
title: dom.getAttribute('title'), title: dom.getAttribute('title'),
alt: dom.getAttribute('alt'), alt: dom.getAttribute('alt'),
path: dom.getAttribute('data-path') path: dom.dataset.path
}) })
} }
], ],
@ -162,12 +163,12 @@ class ImageView {
export default (path?: string): ProseMirrorExtension => ({ export default (path?: string): ProseMirrorExtension => ({
schema: (prev) => ({ schema: (prev) => ({
...prev, ...prev,
nodes: (prev.nodes as any).update('image', imageSchema) nodes: (prev.nodes as OrderedMap<any>).update('image', imageSchema)
}), }),
plugins: (prev, schema) => [...prev, imageInput(schema, path)], plugins: (prev, schema) => [...prev, imageInput(schema, path)],
nodeViews: { nodeViews: {
image: (node, view, getPos) => { image: (node, view, getPos) => {
return new ImageView(node, view, getPos, view.state.schema, path) return new ImageView(node, view, getPos, view.state.schema, path)
} }
} } as unknown as { [key: string]: NodeViewFn }
}) })

View File

@ -1,9 +1,9 @@
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state' import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { Mark, Node, Schema } from 'prosemirror-model' import type { Mark, Node, Schema } from 'prosemirror-model'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const REGEX = /(^|\s)\[(.+)\]\(([^ ]+)(?: "(.+)")?\)/ const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => { const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
let markPos = { from: -1, to: -1 } let markPos = { from: -1, to: -1 }
@ -26,7 +26,7 @@ const markdownLinks = (schema: Schema) =>
init() { init() {
return { schema } return { schema }
}, },
apply(tr, state) { apply(tr, state: any) {
const action = tr.getMeta(this) const action = tr.getMeta(this)
if (action?.pos) { if (action?.pos) {
state.pos = action.pos state.pos = action.pos
@ -54,11 +54,12 @@ const markdownLinks = (schema: Schema) =>
const resolvePos = (view: EditorView, pos: number) => { const resolvePos = (view: EditorView, pos: number) => {
try { try {
return view.state.doc.resolve(pos) return view.state.doc.resolve(pos)
} catch (err) { } catch {
// ignore // ignore
} }
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
const toLink = (view: EditorView, tr: Transaction) => { const toLink = (view: EditorView, tr: Transaction) => {
const sel = view.state.selection const sel = view.state.selection
const state = pluginKey.getState(view.state) const state = pluginKey.getState(view.state)

View File

@ -1,9 +1,10 @@
import { InputRule } from 'prosemirror-inputrules' import { InputRule } from 'prosemirror-inputrules'
import { EditorState } from 'prosemirror-state' import type { EditorState } from 'prosemirror-state'
import { MarkType } from 'prosemirror-model' import type { MarkType } from 'prosemirror-model'
export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = undefined) => export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = null) =>
new InputRule(regexp, (state: EditorState, match: string[], start: number, end: number) => { new InputRule(regexp, (state: EditorState, match: string[], start: number, end: number) => {
let markEnd = end
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
const tr = state.tr const tr = state.tr
if (match[1]) { if (match[1]) {
@ -11,22 +12,16 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = und
const textEnd = textStart + match[1].length const textEnd = textStart + match[1].length
let hasMarks = false let hasMarks = false
state.doc.nodesBetween(textStart, textEnd, (node) => { state.doc.nodesBetween(textStart, textEnd, (node) => {
if (node.marks.length > 0) { hasMarks = node.marks.length > 0
hasMarks = true
return
}
}) })
if (hasMarks) { if (hasMarks) return
return
}
if (textEnd < end) tr.delete(textEnd, end) if (textEnd < end) tr.delete(textEnd, end)
if (textStart > start) tr.delete(start, textStart) if (textStart > start) tr.delete(start, textStart)
end = start + match[1].length markEnd = start + match[1].length
} }
tr.addMark(start, end, nodeType.create(attrs)) tr.addMark(start, markEnd, nodeType.create(attrs))
tr.removeStoredMark(nodeType) tr.removeStoredMark(nodeType)
return tr return tr
}) })

View File

@ -6,8 +6,8 @@ import {
emDash, emDash,
ellipsis ellipsis
} from 'prosemirror-inputrules' } from 'prosemirror-inputrules'
import { NodeType, Schema } from 'prosemirror-model' import type { NodeType, Schema } from 'prosemirror-model'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType) const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
@ -16,10 +16,10 @@ const orderedListRule = (nodeType: NodeType) =>
/^(\d+)\.\s$/, /^(\d+)\.\s$/,
nodeType, nodeType,
(match) => ({ order: +match[1] }), (match) => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order == +match[1] (match, node) => node.childCount + node.attrs.order === +match[1]
) )
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([-+*])\s$/, nodeType) const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([*+-])\s$/, nodeType)
const headingRule = (nodeType: NodeType, maxLevel: number) => const headingRule = (nodeType: NodeType, maxLevel: number) =>
textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({ textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({
@ -27,7 +27,7 @@ const headingRule = (nodeType: NodeType, maxLevel: number) =>
})) }))
const markdownRules = (schema: Schema) => { const markdownRules = (schema: Schema) => {
const rules = smartQuotes.concat(ellipsis, emDash) const rules = [...smartQuotes, ellipsis, emDash]
if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote)) if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote))
if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list)) if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list))
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list)) if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list))

View File

@ -14,13 +14,17 @@ import {
} from 'prosemirror-menu' } from 'prosemirror-menu'
import { wrapInList } from 'prosemirror-schema-list' import { wrapInList } from 'prosemirror-schema-list'
import { NodeSelection } from 'prosemirror-state' import type{ NodeSelection } from 'prosemirror-state'
import { TextField, openPrompt } from './prompt' import { TextField, openPrompt } from './prompt'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
import type { Schema } from 'prosemirror-model'
// Helpers to create specific types of items // Helpers to create specific types of items
const cut = (something) => something.filter(Boolean)
function canInsert(state, nodeType) { function canInsert(state, nodeType) {
const $from = state.selection.$from const $from = state.selection.$from
@ -41,10 +45,7 @@ function insertImageItem(nodeType) {
return canInsert(state, nodeType) return canInsert(state, nodeType)
}, },
run(state, _, view) { run(state, _, view) {
const { from, to } = state.selection const { from, to, node: { attrs } } = state.selection as NodeSelection
let attrs = null
if (state.selection instanceof NodeSelection && state.selection.node.type == nodeType) { attrs = state.selection.node.attrs }
openPrompt({ openPrompt({
title: 'Insert image', title: 'Insert image',
@ -60,8 +61,8 @@ function insertImageItem(nodeType) {
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ') value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
}) })
}, },
callback(attrs) { callback(newAttrs) {
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs))) view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(newAttrs)))
view.focus() view.focus()
} }
}) })
@ -202,7 +203,8 @@ function wrapListItem(nodeType, options) {
// **`fullMenu`**`: [[MenuElement]]` // **`fullMenu`**`: [[MenuElement]]`
// : An array of arrays of menu elements for use as the full menu // : An array of arrays of menu elements for use as the full menu
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar). // for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
export function buildMenuItems(schema) { // eslint-disable-next-line sonarjs/cognitive-complexity
export function buildMenuItems(schema: Schema<any, any>) {
const r: { [key: string]: MenuItem | MenuItem[] } = {} const r: { [key: string]: MenuItem | MenuItem[] } = {}
let type let type
@ -237,7 +239,7 @@ export function buildMenuItems(schema) {
if ((type = schema.marks.link)) r.toggleLink = linkItem(type) if ((type = schema.marks.link)) r.toggleLink = linkItem(type)
if ((type = schema.marks.blockquote)) { if ((type = schema.nodes.image)) r.insertImage = insertImageItem(type) } if ((type = schema.marks.blockquote) && (type = schema.nodes.image)) r.insertImage = insertImageItem(type)
if ((type = schema.nodes.bullet_list)) { if ((type = schema.nodes.bullet_list)) {
r.wrapBulletList = wrapListItem(type, { r.wrapBulletList = wrapListItem(type, {
@ -318,15 +320,18 @@ export function buildMenuItems(schema) {
}) })
} }
const cut = (arr) => arr.filter((x) => x)
r.typeMenu = new Dropdown( r.typeMenu = new Dropdown(
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]), cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
{ label: 'Тт', icon: { { label: 'Тт',
width: 12, class: 'editor-dropdown' // TODO: use this class
height: 12, // FIXME: icon svg code shouldn't be here
path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z" // icon: {
} }) // width: 12,
// r.blockMenu = [] // height: 12,
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
// }
}) as MenuItem
r.blockMenu = []
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])] r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])] r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu) r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu)
@ -339,7 +344,7 @@ export default (): ProseMirrorExtension => ({
...prev, ...prev,
menuBar({ menuBar({
floating: false, floating: false,
content: buildMenuItems(schema).fullMenu content: buildMenuItems(schema).fullMenu as any
}) })
] ]
}) })

View File

@ -1,9 +1,10 @@
import { Plugin } from 'prosemirror-state' import { Plugin, Transaction } from 'prosemirror-state'
import { Fragment, Node, Schema, Slice } from 'prosemirror-model' import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
import { createMarkdownParser } from '../../markdown' import { createMarkdownParser } from '../../markdown'
import { openPrompt } from './prompt'
const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/g const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:\d+)?(\/|\/([\w!#%&+./:=?@-]))?/g
const transform = (schema: Schema, fragment: Fragment) => { const transform = (schema: Schema, fragment: Fragment) => {
const nodes = [] const nodes = []
@ -57,16 +58,15 @@ const pasteMarkdown = (schema: Schema) => {
if (!event.clipboardData) return false if (!event.clipboardData) return false
const text = event.clipboardData.getData('text/plain') const text = event.clipboardData.getData('text/plain')
const html = event.clipboardData.getData('text/html') const html = event.clipboardData.getData('text/html')
// otherwise, if we have html then fallback to the default HTML // otherwise, if we have html then fallback to the default HTML
// parser behavior that comes with Prosemirror. // parser behavior that comes with Prosemirror.
if (text.length === 0 || html) return false if (text.length === 0 || html) return false
event.preventDefault() event.preventDefault()
const node: Node = parser.parse(text)
const paste = parser.parse(text) const fragment = shiftKey ? node.content : transform(schema, node.content)
const slice = paste.slice(0) const openStart = 0 // FIXME
const fragment = shiftKey ? slice.content : transform(schema, slice.content) const openEnd = text.length // FIXME: detect real start and end cursor position
const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd)) const tr: Transaction = view.state.tr.replaceSelection(new Slice(fragment, openStart, openEnd))
view.dispatch(tr) view.dispatch(tr)
return true return true

View File

@ -1,19 +1,20 @@
const prefix = 'ProseMirror-prompt' const prefix = 'ProseMirror-prompt'
// eslint-disable-next-line sonarjs/cognitive-complexity
export function openPrompt(options: any) { export function openPrompt(options: any) {
const wrapper = document.body.appendChild(document.createElement('div')) const wrapper = document.body.appendChild(document.createElement('div'))
wrapper.className = prefix wrapper.className = prefix
const mouseOutside = (e: any) => { const mouseOutside = (ev: MouseEvent) => {
if (!wrapper.contains(e.target)) close() if (!wrapper.contains(ev.target as Node)) close()
} }
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50) setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
const close = () => { const close = () => {
window.removeEventListener('mousedown', mouseOutside) window.removeEventListener('mousedown', mouseOutside)
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper) if (wrapper.parentNode) wrapper.remove()
} }
const domFields: any = [] const domFields: Node[] = []
options.fields.forEach((name) => { options.fields.forEach((name) => {
domFields.push(options.fields[name].render()) domFields.push(options.fields[name].render())
}) })
@ -32,7 +33,7 @@ export function openPrompt(options: any) {
if (options.title) { if (options.title) {
form.appendChild(document.createElement('h5')).textContent = options.title form.appendChild(document.createElement('h5')).textContent = options.title
} }
domFields.forEach((field: any) => { domFields.forEach((field: Node) => {
form.appendChild(document.createElement('div')).appendChild(field) form.appendChild(document.createElement('div')).appendChild(field)
}) })
const buttons = form.appendChild(document.createElement('div')) const buttons = form.appendChild(document.createElement('div'))
@ -59,20 +60,20 @@ export function openPrompt(options: any) {
}) })
form.addEventListener('keydown', (e) => { form.addEventListener('keydown', (e) => {
if (e.keyCode == 27) { if (e.key === 'Escape') {
e.preventDefault() e.preventDefault()
close() close()
} else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) { } else if (e.key === 'Enter' && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
e.preventDefault() e.preventDefault()
submit() submit()
} else if (e.keyCode == 9) { } else if (e.key === 'Tab') {
window.setTimeout(() => { window.setTimeout(() => {
if (!wrapper.contains(document.activeElement)) close() if (!wrapper.contains(document.activeElement)) close()
}, 500) }, 500)
} }
}) })
const input: any = form.elements[0] const input = form.elements[0] as HTMLInputElement
if (input) input.focus() if (input) input.focus()
} }
@ -93,14 +94,13 @@ function getValues(fields: any, domFields: any) {
return result return result
} }
function reportInvalid(dom: any, message: any) { function reportInvalid(dom: HTMLElement, message: string) {
const parent = dom.parentNode const msg: HTMLElement = dom.parentNode.appendChild(document.createElement('div'))
const msg = parent.appendChild(document.createElement('div'))
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px' msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
msg.style.top = dom.offsetTop - 5 + 'px' msg.style.top = dom.offsetTop - 5 + 'px'
msg.className = 'ProseMirror-invalid' msg.className = 'ProseMirror-invalid'
msg.textContent = message msg.textContent = message
setTimeout(() => parent.removeChild(msg), 1500) setTimeout(msg.remove, 1500)
} }
export class Field { export class Field {
@ -147,7 +147,7 @@ export class SelectField extends Field {
this.options.options.forEach((o: { value: string; label: string }) => { this.options.options.forEach((o: { value: string; label: string }) => {
const opt = select.appendChild(document.createElement('option')) const opt = select.appendChild(document.createElement('option'))
opt.value = o.value opt.value = o.value
opt.selected = o.value == this.options.value opt.selected = o.value === this.options.value
opt.label = o.label opt.label = o.label
}) })
return select return select

View File

@ -1,6 +1,6 @@
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const scroll = (view: EditorView) => { const scroll = (view: EditorView) => {
if (!view.state.selection.empty) return false if (!view.state.selection.empty) return false

View File

@ -1,6 +1,6 @@
import { renderGrouped } from "prosemirror-menu"; import { renderGrouped } from "prosemirror-menu";
import { Plugin } from "prosemirror-state"; import { Plugin } from "prosemirror-state";
import { ProseMirrorExtension } from "../helpers"; import type { ProseMirrorExtension } from "../helpers";
import { buildMenuItems } from "./menu"; import { buildMenuItems } from "./menu";
export class SelectionTooltip { export class SelectionTooltip {
@ -10,7 +10,7 @@ export class SelectionTooltip {
this.tooltip = document.createElement("div"); this.tooltip = document.createElement("div");
this.tooltip.className = "tooltip"; this.tooltip.className = "tooltip";
view.dom.parentNode.appendChild(this.tooltip); view.dom.parentNode.appendChild(this.tooltip);
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu); const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any);
this.tooltip.appendChild(dom); this.tooltip.appendChild(dom);
this.update(view, null); this.update(view, null);
} }

View File

@ -1,9 +1,9 @@
import { inputRules } from 'prosemirror-inputrules' import { inputRules } from 'prosemirror-inputrules'
import { MarkType } from 'prosemirror-model' import type { MarkType } from 'prosemirror-model'
import { markInputRule } from './mark-input-rule' import { markInputRule } from './mark-input-rule'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
const strikethroughRule = (nodeType: MarkType) => markInputRule(/(?:~~)(.+)(?:~~)$/, nodeType) const strikethroughRule = (nodeType: MarkType) => markInputRule(/~{2}(.+)~{2}$/, nodeType)
const strikethroughSchema = { const strikethroughSchema = {
strikethrough: { strikethrough: {

View File

@ -1,15 +1,15 @@
import { EditorState, Selection } from 'prosemirror-state' import { EditorState, Selection } from 'prosemirror-state'
import { Node, Schema, ResolvedPos } from 'prosemirror-model' import type { Node, Schema, ResolvedPos } from 'prosemirror-model'
import { InputRule, inputRules } from 'prosemirror-inputrules' import { InputRule, inputRules } from 'prosemirror-inputrules'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { ProseMirrorExtension } from '../helpers' import type { ProseMirrorExtension } from '../helpers'
export const tableInputRule = (schema: Schema) => export const tableInputRule = (schema: Schema) =>
new InputRule( new InputRule(
new RegExp('^\\|{2,}\\s$'), new RegExp('^\\|{2,}\\s$'),
(state: EditorState, match: string[], start: number, end: number) => { (state: EditorState, match: string[], start: number, end: number) => {
const tr = state.tr const tr = state.tr
const columns = [...Array(match[0].trim().length - 1)] const columns = Array.from({length: match[0].trim().length - 1})
const headers = columns.map(() => schema.node(schema.nodes.table_header, {})) const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {})) const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
const table = schema.node(schema.nodes.table, {}, [ const table = schema.node(schema.nodes.table, {}, [
@ -176,6 +176,7 @@ export default (): ProseMirrorExtension => ({
...prev, ...prev,
nodes: (prev.nodes as any).append(tableSchema) nodes: (prev.nodes as any).append(tableSchema)
}), }),
// eslint-disable-next-line sonarjs/cognitive-complexity
plugins: (prev, schema) => [ plugins: (prev, schema) => [
keymap({ keymap({
'Ctrl-Enter': (state, dispatch) => { 'Ctrl-Enter': (state, dispatch) => {

View File

@ -1,10 +1,9 @@
import { DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model' import { DOMOutputSpec, DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
import { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import { wrappingInputRule } from 'prosemirror-inputrules' import { wrappingInputRule , inputRules } from 'prosemirror-inputrules'
import { splitListItem } from 'prosemirror-schema-list' import { splitListItem } from 'prosemirror-schema-list'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
import { inputRules } from 'prosemirror-inputrules' import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
import { ProseMirrorExtension } from '../helpers'
const todoListRule = (nodeType: NodeType) => const todoListRule = (nodeType: NodeType) =>
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({ wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
@ -54,13 +53,13 @@ class TodoItemView {
getPos: () => number getPos: () => number
constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) { constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {
const dom = node.type.spec.toDOM(node) const dom: DOMOutputSpec = node.type.spec.toDOM(node)
const res = DOMSerializer.renderSpec(document, dom) const res = DOMSerializer.renderSpec(document, dom)
this.dom = res.dom this.dom = res.dom
this.contentDOM = res.contentDOM this.contentDOM = res.contentDOM
this.view = view this.view = view
this.getPos = getPos this.getPos = getPos;
;(this.dom as Element).querySelector('input').onclick = this.handleClick.bind(this) (this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
} }
handleClick(e: MouseEvent) { handleClick(e: MouseEvent) {
@ -87,8 +86,8 @@ export default (): ProseMirrorExtension => ({
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] }) inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
], ],
nodeViews: { nodeViews: {
todo_item: (node, view, getPos) => { todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
return new TodoItemView(node, view, getPos) return new TodoItemView(node, view, getPos)
} }
} } as unknown as { [key:string]: NodeViewFn }
}) })

View File

@ -13,26 +13,13 @@ import markdown from './extension/markdown'
import pasteMarkdown from './extension/paste-markdown' import pasteMarkdown from './extension/paste-markdown'
import table from './extension/table' import table from './extension/table'
import collab from './extension/collab' import collab from './extension/collab'
import type { Config, YOptions } from '../store/context' import type { Collab, Config, ExtensionsProps, YOptions } from '../store/context'
import selectionMenu from './extension/selection' import selectionMenu from './extension/selection'
import type { Command } from 'prosemirror-state'
import placeholder from './extension/placeholder' import placeholder from './extension/placeholder'
import todoList from './extension/todo-list' import todoList from './extension/todo-list'
import strikethrough from './extension/strikethrough' import strikethrough from './extension/strikethrough'
import scrollPlugin from './extension/scroll' import scrollPlugin from './extension/scroll'
interface ExtensionsProps {
data?: unknown
keymap?: { [key: string]: Command }
config: Config
markdown: boolean
path?: string
y?: YOptions
schema?: Schema
collab?: any
typewriterMode?: boolean
}
const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({ const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({
plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev) plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev)
}) })

View File

@ -6,26 +6,19 @@ import { selectAll, deleteSelection } from 'prosemirror-commands'
import { undo as yUndo, redo as yRedo } from 'y-prosemirror' import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup' import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
import { State, File, Config, ServiceError, newState } from './context' import { State, Draft, Config, ServiceError, newState, ExtensionsProps } from './context'
import { mod } from '../env' import { mod } from '../env'
import { serialize, createMarkdownParser } from '../markdown' import { serialize, createMarkdownParser } from '../markdown'
import db from '../db' import db from '../db'
import { isEmpty, isInitialized } from '../prosemirror/helpers' import { isEmpty, isInitialized } from '../prosemirror/helpers'
const isText = (x) => x && x.doc && x.selection const isText = (x) => x && x.doc && x.selection
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.files) const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
const isFile = (x): boolean => x && (x.text || x.path) const isDraft = (x): boolean => x && (x.text || x.path)
export const createCtrl = (initial: State): [Store<State>, any] => { export const createCtrl = (initial: State): [Store<State>, any] => {
const [store, setState] = createStore(initial) const [store, setState] = createStore(initial)
const onDiscard = () => {
discard()
return true
}
const onToggleMarkdown = () => toggleMarkdown()
const onUndo = () => { const onUndo = () => {
if (!isInitialized(store.text)) return if (!isInitialized(store.text)) return
const text = store.text as EditorState const text = store.text as EditorState
@ -34,56 +27,112 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
} else { } else {
undo(text, store.editorView.dispatch) undo(text, store.editorView.dispatch)
} }
return true return true
} }
const onRedo = () => { const onRedo = () => {
if (!isInitialized(store.text)) return if (!isInitialized(store.text)) return false
const text = store.text as EditorState const text = store.text as EditorState
if (store.collab?.started) { if (store.collab?.started) {
yRedo(text) yRedo(text)
} else { } else {
redo(text, store.editorView.dispatch) redo(text, store.editorView.dispatch)
} }
return true
}
const discard = () => {
if (store.path) {
discardText()
} else if (store.drafts.length > 0 && isEmpty(store.text)) {
discardText()
} else {
selectAll(store.editorView.state, store.editorView.dispatch)
deleteSelection(store.editorView.state, store.editorView.dispatch)
}
return true
}
const toggleMarkdown = () => {
const state = unwrap(store)
const editorState = store.text as EditorState
const markdown = !state.markdown
const selection = { type: 'text', anchor: 1, head: 1 }
let doc: any
if (markdown) {
const lines = serialize(editorState).split('\n')
const nodes = lines.map((text) => text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' })
doc = { type: 'doc', content: nodes }
} else {
const schema = createSchema({
config: state.config,
path: state.path,
y: state.collab?.y,
markdown,
keymap
})
const parser = createMarkdownParser(schema)
let textContent = ''
editorState.doc.forEach((node) => {
textContent += `${node.textContent}\n`
})
const text = parser.parse(textContent)
doc = text.toJSON()
}
const extensions = createExtensions({
config: state.config,
markdown,
path: state.path,
keymap,
y: state.collab?.y
})
setState({
text: { selection, doc },
extensions,
markdown
})
return true return true
} }
const keymap = { const keymap = {
[`${mod}-w`]: onDiscard, [`${mod}-w`]: discard,
[`${mod}-z`]: onUndo, [`${mod}-z`]: onUndo,
[`Shift-${mod}-z`]: onRedo, [`Shift-${mod}-z`]: onRedo,
[`${mod}-y`]: onRedo, [`${mod}-y`]: onRedo,
[`${mod}-m`]: onToggleMarkdown [`${mod}-m`]: toggleMarkdown
} } as ExtensionsProps['keymap']
const createTextFromFile = async (file: File) => { const createTextFromDraft = async (draft: Draft) => {
const state = unwrap(store) const state = unwrap(store)
const extensions = createExtensions({ const extensions = createExtensions({
config: state.config, config: state.config,
markdown: file.markdown, markdown: draft.markdown,
path: file.path, path: draft.path,
keymap keymap
}) })
return { return {
text: file.text, text: draft.text,
extensions, extensions,
lastModified: file.lastModified ? new Date(file.lastModified) : undefined, lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined,
path: file.path, path: draft.path,
markdown: file.markdown markdown: draft.markdown
} }
} }
const addToFiles = (files: File[], prev: State) => { const addToDrafts = (drafts: Draft[], prev: State) => {
const text = prev.path ? undefined : (prev.text as EditorState).toJSON() const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
return [ return [
...files, ...drafts,
{ {
text, text,
lastModified: prev.lastModified?.toISOString(), lastModified: prev.lastModified,
path: prev.path, path: prev.path,
markdown: prev.markdown markdown: prev.markdown
} }
@ -92,12 +141,12 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
const discardText = async () => { const discardText = async () => {
const state = unwrap(store) const state = unwrap(store)
const index = state.files.length - 1 const index = state.drafts.length - 1
const file = index !== -1 ? state.files[index] : undefined const draft = index !== -1 ? state.drafts[index] : undefined
let next: Partial<State> let next: Partial<State>
if (file) { if (draft) {
next = await createTextFromFile(file) next = await createTextFromDraft(draft)
} else { } else {
const extensions = createExtensions({ const extensions = createExtensions({
config: state.config ?? store.config, config: state.config ?? store.config,
@ -114,12 +163,12 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
} }
} }
const files = state.files.filter((f: File) => f !== file) const drafts = state.drafts.filter((f: Draft) => f !== draft)
setState({ setState({
files, drafts,
...next, ...next,
collab: file ? undefined : state.collab, collab: draft ? undefined : state.collab,
error: undefined error: undefined
}) })
} }
@ -171,14 +220,14 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
newst.lastModified = new Date(newst.lastModified) newst.lastModified = new Date(newst.lastModified)
} }
for (const file of parsed.files) { for (const draft of parsed.drafts || []) {
if (!isFile(file)) { if (!isDraft(draft)) {
throw new ServiceError('invalid_file', file) throw new ServiceError('invalid_draft', draft)
} }
} }
if (!isState(newst)) { if (!isState(newst)) {
throw new ServiceError('invalid_state', newState) throw new ServiceError('invalid_state', newst)
} }
return newst return newst
@ -190,7 +239,7 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
setState({ setState({
...newState(), ...newState(),
loading: 'initialized', loading: 'initialized',
files: [], drafts: [],
fullscreen: store.fullscreen, fullscreen: store.fullscreen,
lastModified: new Date(), lastModified: new Date(),
error: undefined, error: undefined,
@ -198,17 +247,6 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
}) })
} }
const discard = async () => {
if (store.path) {
await discardText()
} else if (store.files.length > 0 && isEmpty(store.text)) {
await discardText()
} else {
selectAll(store.editorView.state, store.editorView.dispatch)
deleteSelection(store.editorView.state, store.editorView.dispatch)
}
}
const init = async () => { const init = async () => {
let data = await fetchData() let data = await fetchData()
try { try {
@ -235,9 +273,9 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
} }
const saveState = () => debounce(async (state: State) => { const saveState = () => debounce(async (state: State) => {
const data: any = { const data: State = {
lastModified: state.lastModified, lastModified: state.lastModified,
files: state.files, drafts: state.drafts,
config: state.config, config: state.config,
path: state.path, path: state.path,
markdown: state.markdown, markdown: state.markdown,
@ -283,14 +321,14 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
let newst = state let newst = state
if ((backup && !isEmpty(state.text)) || state.path) { if ((backup && !isEmpty(state.text)) || state.path) {
let files = state.files let drafts = state.drafts
if (!state.error) { if (!state.error) {
files = addToFiles(files, state) drafts = addToDrafts(drafts, state)
} }
newst = { newst = {
...state, ...state,
files, drafts,
lastModified: undefined, lastModified: undefined,
path: undefined, path: undefined,
error: undefined error: undefined
@ -317,53 +355,6 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
window.history.replaceState(null, '', '/') window.history.replaceState(null, '', '/')
} }
const toggleMarkdown = () => {
const state = unwrap(store)
const editorState = store.text as EditorState
const markdown = !state.markdown
const selection = { type: 'text', anchor: 1, head: 1 }
let doc: any
if (markdown) {
const lines = serialize(editorState).split('\n')
const nodes = lines.map((text) => {
return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
})
doc = { type: 'doc', content: nodes }
} else {
const schema = createSchema({
config: state.config,
path: state.path,
y: state.collab?.y,
markdown,
keymap
})
const parser = createMarkdownParser(schema)
let textContent = ''
editorState.doc.forEach((node) => {
textContent += `${node.textContent}\n`
})
const text = parser.parse(textContent)
doc = text.toJSON()
}
const extensions = createExtensions({
config: state.config,
markdown,
path: state.path,
keymap,
y: state.collab?.y
})
setState({
text: { selection, doc },
extensions,
markdown
})
}
const updateConfig = (config: Partial<Config>) => { const updateConfig = (config: Partial<Config>) => {
const state = unwrap(store) const state = unwrap(store)
const extensions = createExtensions({ const extensions = createExtensions({

View File

@ -5,10 +5,22 @@ import type { WebrtcProvider } from 'y-webrtc'
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers' import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
import type { Command, EditorState } from 'prosemirror-state' import type { Command, EditorState } from 'prosemirror-state'
import type { EditorView } from 'prosemirror-view' import type { EditorView } from 'prosemirror-view'
import type { Schema } from 'prosemirror-model'
export interface ExtensionsProps {
data?: unknown
keymap?: { [key: string]: Command }
config: Config
markdown: boolean
path?: string
y?: YOptions
schema?: Schema
collab?: Collab
typewriterMode?: boolean
}
export interface Args { export interface Args {
cwd?: string; cwd?: string;
file?: string; draft?: string;
room?: string; room?: string;
text?: any; text?: any;
} }
@ -56,11 +68,11 @@ export interface State {
extensions?: ProseMirrorExtension[]; extensions?: ProseMirrorExtension[];
markdown?: boolean; markdown?: boolean;
lastModified?: Date; lastModified?: Date;
files: File[]; drafts: Draft[];
config: Config; config: Config;
error?: ErrorObject; error?: ErrorObject;
loading: LoadingType; loading?: LoadingType;
fullscreen: boolean; fullscreen?: boolean;
collab?: Collab; collab?: Collab;
path?: string; path?: string;
args?: Args; args?: Args;
@ -76,13 +88,6 @@ export interface Draft {
extensions?: ProseMirrorExtension[] extensions?: ProseMirrorExtension[]
} }
export interface File {
text?: { [key: string]: any };
lastModified?: string;
path?: string;
markdown?: boolean;
}
export class ServiceError extends Error { export class ServiceError extends Error {
public errorObject: ErrorObject public errorObject: ErrorObject
constructor(id: string, props: unknown) { constructor(id: string, props: unknown) {
@ -97,7 +102,7 @@ export const useState = () => useContext(StateContext)
export const newState = (props: Partial<State> = {}): State => ({ export const newState = (props: Partial<State> = {}): State => ({
extensions: [], extensions: [],
files: [], drafts: [],
loading: 'loading', loading: 'loading',
fullscreen: false, fullscreen: false,
markdown: false, markdown: false,

View File

@ -142,7 +142,7 @@
cursor: not-allowed; cursor: not-allowed;
} }
&.file { &.draft {
color: rgba(255,255,255,0.5); color: rgba(255,255,255,0.5);
line-height: 1.4; line-height: 1.4;
margin: 0 0 1em 1.5em; margin: 0 0 1em 1.5em;