refactored and linted
This commit is contained in:
parent
92906931da
commit
5d5706b5d7
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}>Советы и предложения</span>
|
<span class='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</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' : ''}>
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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) => [
|
||||||
|
|
|
@ -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()
|
||||||
]
|
]
|
||||||
|
|
|
@ -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' })
|
||||||
})
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 }
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user