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'}>
|
||||
<InvalidState title='Invalid Config' />
|
||||
</Match>
|
||||
<Match when={store.error.id === 'invalid_file'}>
|
||||
<InvalidState title='Invalid File' />
|
||||
<Match when={store.error.id === 'invalid_draft'}>
|
||||
<InvalidState title='Invalid Draft' />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
||||
import { unwrap } from 'solid-js/store'
|
||||
import { undo, redo } from 'prosemirror-history'
|
||||
import { File, useState } from '../store/context'
|
||||
import { Draft, useState } from '../store/context'
|
||||
import { mod } from '../env'
|
||||
import * as remote from '../remote'
|
||||
import { isEmpty } from '../prosemirror/helpers'
|
||||
import { Styled } from './Layout'
|
||||
import type { Styled } from './Layout'
|
||||
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>
|
||||
|
||||
|
@ -16,8 +16,8 @@ const Link = (
|
|||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||
) => (
|
||||
<button
|
||||
class={`sidebar-link${props.className ? ` ${props.className}` : ''}`}
|
||||
style={{ marginBottom: props.withMargin ? '10px' : '' }}
|
||||
class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
|
||||
style={{ "margin-bottom": props.withMargin ? '10px' : '' }}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
title={props.title}
|
||||
|
@ -34,10 +34,10 @@ export const Sidebar = () => {
|
|||
document.body.classList.toggle('dark')
|
||||
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 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 onUndo = () => undo(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)
|
||||
}
|
||||
|
||||
const FileLink = (p: { file: File }) => {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const DraftLink = (p: { draft: Draft }) => {
|
||||
const length = 100
|
||||
let content = ''
|
||||
const getContent = (node: any) => {
|
||||
|
@ -65,7 +66,7 @@ export const Sidebar = () => {
|
|||
}
|
||||
|
||||
if (content.length > length) {
|
||||
content = content.substring(0, length) + '...'
|
||||
content = content.slice(0, Math.max(0, length)) + '...'
|
||||
return content
|
||||
}
|
||||
|
||||
|
@ -83,46 +84,47 @@ export const Sidebar = () => {
|
|||
}
|
||||
|
||||
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 (
|
||||
<Link className='file' onClick={() => onOpenFile(p.file)} data-testid='open'>
|
||||
{text()} {p.file.path && '📎'}
|
||||
// eslint-disable-next-line solid/no-react-specific-props
|
||||
<Link className='draft' onClick={() => onOpenDraft(p.draft)} data-testid='open'>
|
||||
{text()} {p.draft.path && '📎'}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Keys = ({ keys }: { keys: string[] }) => (
|
||||
const Keys = (props) => (
|
||||
<span>
|
||||
{keys.map((k) => (
|
||||
<For each={props.keys}>{(k: Element) => (
|
||||
<i>{k}</i>
|
||||
))}
|
||||
)}</For>
|
||||
</span>
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
setLastAction(undefined)
|
||||
setLastAction()
|
||||
}, store.lastModified)
|
||||
|
||||
createEffect(() => {
|
||||
if (!lastAction()) return
|
||||
const id = setTimeout(() => {
|
||||
setLastAction(undefined)
|
||||
setLastAction()
|
||||
}, 1000)
|
||||
onCleanup(() => clearTimeout(id))
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||
<span className='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</span>
|
||||
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||
<span class='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</span>
|
||||
|
||||
<Off onClick={() => editorView().focus()}>
|
||||
<div className='sidebar-closer' onClick={toggleSidebar}/>
|
||||
<div class='sidebar-closer' onClick={toggleSidebar}/>
|
||||
<Show when={true}>
|
||||
<div>
|
||||
{store.path && (
|
||||
<Label>
|
||||
<i>({store.path.substring(store.path.length - 24)})</i>
|
||||
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
||||
</Label>
|
||||
)}
|
||||
<Link>
|
||||
|
@ -142,26 +144,26 @@ export const Sidebar = () => {
|
|||
</div>
|
||||
<Link
|
||||
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'
|
||||
>
|
||||
{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']} />
|
||||
</Link>
|
||||
<Link onClick={onUndo}>
|
||||
Undo <Keys keys={[mod, 'z']} />
|
||||
</Link>
|
||||
<Link onClick={onRedo}>
|
||||
Redo <Keys keys={[mod, ...['Shift', 'z']]} />
|
||||
Redo <Keys keys={[mod, 'Shift', 'z']} />
|
||||
</Link>
|
||||
<Link onClick={onToggleMarkdown} data-testid='markdown'>
|
||||
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
||||
</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>
|
||||
<p>
|
||||
<For each={store.files}>{(file) => <FileLink file={file} />}</For>
|
||||
<For each={store.drafts}>{(draft) => <DraftLink draft={draft} />}</For>
|
||||
</p>
|
||||
</Show>
|
||||
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { history } from 'prosemirror-history'
|
|||
import { dropCursor } from 'prosemirror-dropcursor'
|
||||
import { buildKeymap } from 'prosemirror-example-setup'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const plainSchema = new Schema({
|
||||
nodes: {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { inputRules } from 'prosemirror-inputrules'
|
||||
import { Mark, MarkType } from 'prosemirror-model'
|
||||
import { EditorState, Transaction } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import type { Mark, MarkType } from 'prosemirror-model'
|
||||
import type { EditorState, Transaction } from 'prosemirror-state'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { markInputRule } from './mark-input-rule'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const blank = '\xa0'
|
||||
const blank = '\u00A0'
|
||||
|
||||
const onArrow =
|
||||
(dir: 'left' | 'right') =>
|
||||
|
@ -36,7 +36,7 @@ const codeKeymap = {
|
|||
ArrowRight: onArrow('right')
|
||||
}
|
||||
|
||||
const codeRule = (nodeType: MarkType) => markInputRule(/(?:`)([^`]+)(?:`)$/, nodeType)
|
||||
const codeRule = (nodeType: MarkType) => markInputRule(/`([^`]+)`$/, nodeType)
|
||||
|
||||
export default (): ProseMirrorExtension => ({
|
||||
plugins: (prev, schema) => [
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import { YOptions } from '../../store/context'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
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')
|
||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||
cursor.setAttribute('style', `border-color: ${user.background}`)
|
||||
|
@ -19,7 +21,6 @@ export default (y: YOptions): ProseMirrorExtension => ({
|
|||
? [
|
||||
...prev,
|
||||
ySyncPlugin(y.type),
|
||||
// @ts-ignore
|
||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||
yUndoPlugin()
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Plugin, NodeSelection } from 'prosemirror-state'
|
||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const handleIcon = `
|
||||
<svg viewBox="0 0 10 10" height="14" width="14">
|
||||
|
@ -22,11 +22,9 @@ const handlePlugin = new Plugin({
|
|||
decorations(state) {
|
||||
const decos = []
|
||||
state.doc.forEach((node, pos) => {
|
||||
decos.push(Decoration.widget(pos + 1, createDragHandle))
|
||||
decos.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: 'draggable'
|
||||
})
|
||||
Decoration.widget(pos + 1, createDragHandle),
|
||||
Decoration.node(pos, pos + node.nodeSize, { class: 'draggable' })
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import { Node, Schema } from 'prosemirror-model'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { Node, Schema } from 'prosemirror-model'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||
import type OrderedMap from 'orderedmap'
|
||||
|
||||
const REGEX = /^!\[([^[\]]*?)\]\((.+?)\)\s+/
|
||||
const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
|
||||
const MAX_MATCH = 500
|
||||
|
||||
const isUrl = (str: string) => {
|
||||
try {
|
||||
const url = new URL(str)
|
||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isBlank = (text: string) => text === ' ' || text === '\xa0'
|
||||
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
||||
|
||||
const imageInput = (schema: Schema, path?: string) =>
|
||||
new Plugin({
|
||||
|
@ -29,7 +30,7 @@ const imageInput = (schema: Schema, path?: string) =>
|
|||
Math.max(0, $from.parentOffset - MAX_MATCH),
|
||||
$from.parentOffset,
|
||||
null,
|
||||
'\ufffc'
|
||||
'\uFFFC'
|
||||
) + text
|
||||
|
||||
const match = REGEX.exec(textBefore)
|
||||
|
@ -65,11 +66,11 @@ const imageSchema = {
|
|||
parseDOM: [
|
||||
{
|
||||
tag: 'img[src]',
|
||||
getAttrs: (dom: Element) => ({
|
||||
getAttrs: (dom: HTMLElement) => ({
|
||||
src: dom.getAttribute('src'),
|
||||
title: dom.getAttribute('title'),
|
||||
alt: dom.getAttribute('alt'),
|
||||
path: dom.getAttribute('data-path')
|
||||
path: dom.dataset.path
|
||||
})
|
||||
}
|
||||
],
|
||||
|
@ -162,12 +163,12 @@ class ImageView {
|
|||
export default (path?: string): ProseMirrorExtension => ({
|
||||
schema: (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)],
|
||||
nodeViews: {
|
||||
image: (node, view, getPos) => {
|
||||
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 { EditorView } from 'prosemirror-view'
|
||||
import { Mark, Node, Schema } from 'prosemirror-model'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { Mark, Node, Schema } from 'prosemirror-model'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const REGEX = /(^|\s)\[(.+)\]\(([^ ]+)(?: "(.+)")?\)/
|
||||
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
||||
|
||||
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
||||
let markPos = { from: -1, to: -1 }
|
||||
|
@ -26,7 +26,7 @@ const markdownLinks = (schema: Schema) =>
|
|||
init() {
|
||||
return { schema }
|
||||
},
|
||||
apply(tr, state) {
|
||||
apply(tr, state: any) {
|
||||
const action = tr.getMeta(this)
|
||||
if (action?.pos) {
|
||||
state.pos = action.pos
|
||||
|
@ -54,11 +54,12 @@ const markdownLinks = (schema: Schema) =>
|
|||
const resolvePos = (view: EditorView, pos: number) => {
|
||||
try {
|
||||
return view.state.doc.resolve(pos)
|
||||
} catch (err) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const toLink = (view: EditorView, tr: Transaction) => {
|
||||
const sel = view.state.selection
|
||||
const state = pluginKey.getState(view.state)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import type { EditorState } from 'prosemirror-state'
|
||||
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) => {
|
||||
let markEnd = end
|
||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||
const tr = state.tr
|
||||
if (match[1]) {
|
||||
|
@ -11,22 +12,16 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = und
|
|||
const textEnd = textStart + match[1].length
|
||||
let hasMarks = false
|
||||
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
||||
if (node.marks.length > 0) {
|
||||
hasMarks = true
|
||||
return
|
||||
}
|
||||
hasMarks = node.marks.length > 0
|
||||
})
|
||||
|
||||
if (hasMarks) {
|
||||
return
|
||||
}
|
||||
|
||||
if (hasMarks) return
|
||||
if (textEnd < end) tr.delete(textEnd, end)
|
||||
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)
|
||||
return tr
|
||||
})
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
emDash,
|
||||
ellipsis
|
||||
} from 'prosemirror-inputrules'
|
||||
import { NodeType, Schema } from 'prosemirror-model'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { NodeType, Schema } from 'prosemirror-model'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
||||
|
||||
|
@ -16,10 +16,10 @@ const orderedListRule = (nodeType: NodeType) =>
|
|||
/^(\d+)\.\s$/,
|
||||
nodeType,
|
||||
(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) =>
|
||||
textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({
|
||||
|
@ -27,7 +27,7 @@ const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
|||
}))
|
||||
|
||||
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.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list))
|
||||
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list))
|
||||
|
|
|
@ -14,13 +14,17 @@ import {
|
|||
} from 'prosemirror-menu'
|
||||
|
||||
import { wrapInList } from 'prosemirror-schema-list'
|
||||
import { NodeSelection } from 'prosemirror-state'
|
||||
import type{ NodeSelection } from 'prosemirror-state'
|
||||
|
||||
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
|
||||
|
||||
|
||||
const cut = (something) => something.filter(Boolean)
|
||||
|
||||
function canInsert(state, nodeType) {
|
||||
const $from = state.selection.$from
|
||||
|
||||
|
@ -41,10 +45,7 @@ function insertImageItem(nodeType) {
|
|||
return canInsert(state, nodeType)
|
||||
},
|
||||
run(state, _, view) {
|
||||
const { from, to } = state.selection
|
||||
let attrs = null
|
||||
|
||||
if (state.selection instanceof NodeSelection && state.selection.node.type == nodeType) { attrs = state.selection.node.attrs }
|
||||
const { from, to, node: { attrs } } = state.selection as NodeSelection
|
||||
|
||||
openPrompt({
|
||||
title: 'Insert image',
|
||||
|
@ -60,8 +61,8 @@ function insertImageItem(nodeType) {
|
|||
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
||||
})
|
||||
},
|
||||
callback(attrs) {
|
||||
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))
|
||||
callback(newAttrs) {
|
||||
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(newAttrs)))
|
||||
view.focus()
|
||||
}
|
||||
})
|
||||
|
@ -202,7 +203,8 @@ function wrapListItem(nodeType, options) {
|
|||
// **`fullMenu`**`: [[MenuElement]]`
|
||||
// : 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).
|
||||
export function buildMenuItems(schema) {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function buildMenuItems(schema: Schema<any, any>) {
|
||||
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
||||
let type
|
||||
|
||||
|
@ -237,7 +239,7 @@ export function buildMenuItems(schema) {
|
|||
|
||||
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)) {
|
||||
r.wrapBulletList = wrapListItem(type, {
|
||||
|
@ -318,15 +320,18 @@ export function buildMenuItems(schema) {
|
|||
})
|
||||
}
|
||||
|
||||
const cut = (arr) => arr.filter((x) => x)
|
||||
r.typeMenu = new Dropdown(
|
||||
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
|
||||
{ label: 'Тт', icon: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
||||
} })
|
||||
// r.blockMenu = []
|
||||
{ label: 'Тт',
|
||||
class: 'editor-dropdown' // TODO: use this class
|
||||
// FIXME: icon svg code shouldn't be here
|
||||
// icon: {
|
||||
// width: 12,
|
||||
// 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.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
||||
r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu)
|
||||
|
@ -339,7 +344,7 @@ export default (): ProseMirrorExtension => ({
|
|||
...prev,
|
||||
menuBar({
|
||||
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 { ProseMirrorExtension } from '../helpers'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
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 nodes = []
|
||||
|
@ -57,16 +58,15 @@ const pasteMarkdown = (schema: Schema) => {
|
|||
if (!event.clipboardData) return false
|
||||
const text = event.clipboardData.getData('text/plain')
|
||||
const html = event.clipboardData.getData('text/html')
|
||||
|
||||
// otherwise, if we have html then fallback to the default HTML
|
||||
// parser behavior that comes with Prosemirror.
|
||||
if (text.length === 0 || html) return false
|
||||
event.preventDefault()
|
||||
|
||||
const paste = parser.parse(text)
|
||||
const slice = paste.slice(0)
|
||||
const fragment = shiftKey ? slice.content : transform(schema, slice.content)
|
||||
const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd))
|
||||
const node: Node = parser.parse(text)
|
||||
const fragment = shiftKey ? node.content : transform(schema, node.content)
|
||||
const openStart = 0 // FIXME
|
||||
const openEnd = text.length // FIXME: detect real start and end cursor position
|
||||
const tr: Transaction = view.state.tr.replaceSelection(new Slice(fragment, openStart, openEnd))
|
||||
|
||||
view.dispatch(tr)
|
||||
return true
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
const prefix = 'ProseMirror-prompt'
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function openPrompt(options: any) {
|
||||
const wrapper = document.body.appendChild(document.createElement('div'))
|
||||
wrapper.className = prefix
|
||||
|
||||
const mouseOutside = (e: any) => {
|
||||
if (!wrapper.contains(e.target)) close()
|
||||
const mouseOutside = (ev: MouseEvent) => {
|
||||
if (!wrapper.contains(ev.target as Node)) close()
|
||||
}
|
||||
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
||||
const close = () => {
|
||||
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) => {
|
||||
domFields.push(options.fields[name].render())
|
||||
})
|
||||
|
@ -32,7 +33,7 @@ export function openPrompt(options: any) {
|
|||
if (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)
|
||||
})
|
||||
const buttons = form.appendChild(document.createElement('div'))
|
||||
|
@ -59,20 +60,20 @@ export function openPrompt(options: any) {
|
|||
})
|
||||
|
||||
form.addEventListener('keydown', (e) => {
|
||||
if (e.keyCode == 27) {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
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()
|
||||
submit()
|
||||
} else if (e.keyCode == 9) {
|
||||
} else if (e.key === 'Tab') {
|
||||
window.setTimeout(() => {
|
||||
if (!wrapper.contains(document.activeElement)) close()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
const input: any = form.elements[0]
|
||||
const input = form.elements[0] as HTMLInputElement
|
||||
if (input) input.focus()
|
||||
}
|
||||
|
||||
|
@ -93,14 +94,13 @@ function getValues(fields: any, domFields: any) {
|
|||
return result
|
||||
}
|
||||
|
||||
function reportInvalid(dom: any, message: any) {
|
||||
const parent = dom.parentNode
|
||||
const msg = parent.appendChild(document.createElement('div'))
|
||||
function reportInvalid(dom: HTMLElement, message: string) {
|
||||
const msg: HTMLElement = dom.parentNode.appendChild(document.createElement('div'))
|
||||
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
||||
msg.style.top = dom.offsetTop - 5 + 'px'
|
||||
msg.className = 'ProseMirror-invalid'
|
||||
msg.textContent = message
|
||||
setTimeout(() => parent.removeChild(msg), 1500)
|
||||
setTimeout(msg.remove, 1500)
|
||||
}
|
||||
|
||||
export class Field {
|
||||
|
@ -147,7 +147,7 @@ export class SelectField extends Field {
|
|||
this.options.options.forEach((o: { value: string; label: string }) => {
|
||||
const opt = select.appendChild(document.createElement('option'))
|
||||
opt.value = o.value
|
||||
opt.selected = o.value == this.options.value
|
||||
opt.selected = o.value === this.options.value
|
||||
opt.label = o.label
|
||||
})
|
||||
return select
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const scroll = (view: EditorView) => {
|
||||
if (!view.state.selection.empty) return false
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { renderGrouped } from "prosemirror-menu";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { ProseMirrorExtension } from "../helpers";
|
||||
import type { ProseMirrorExtension } from "../helpers";
|
||||
import { buildMenuItems } from "./menu";
|
||||
|
||||
export class SelectionTooltip {
|
||||
|
@ -10,7 +10,7 @@ export class SelectionTooltip {
|
|||
this.tooltip = document.createElement("div");
|
||||
this.tooltip.className = "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.update(view, null);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { inputRules } from 'prosemirror-inputrules'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
import type { MarkType } from 'prosemirror-model'
|
||||
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 = {
|
||||
strikethrough: {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
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 { keymap } from 'prosemirror-keymap'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
export const tableInputRule = (schema: Schema) =>
|
||||
new InputRule(
|
||||
new RegExp('^\\|{2,}\\s$'),
|
||||
(state: EditorState, match: string[], start: number, end: number) => {
|
||||
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 cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
||||
const table = schema.node(schema.nodes.table, {}, [
|
||||
|
@ -176,6 +176,7 @@ export default (): ProseMirrorExtension => ({
|
|||
...prev,
|
||||
nodes: (prev.nodes as any).append(tableSchema)
|
||||
}),
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
plugins: (prev, schema) => [
|
||||
keymap({
|
||||
'Ctrl-Enter': (state, dispatch) => {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||
import { DOMOutputSpec, DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import { wrappingInputRule , inputRules } from 'prosemirror-inputrules'
|
||||
import { splitListItem } from 'prosemirror-schema-list'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { inputRules } from 'prosemirror-inputrules'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const todoListRule = (nodeType: NodeType) =>
|
||||
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
||||
|
@ -54,13 +53,13 @@ class TodoItemView {
|
|||
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)
|
||||
this.dom = res.dom
|
||||
this.contentDOM = res.contentDOM
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
;(this.dom as Element).querySelector('input').onclick = this.handleClick.bind(this)
|
||||
this.getPos = getPos;
|
||||
(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
|
||||
}
|
||||
|
||||
handleClick(e: MouseEvent) {
|
||||
|
@ -87,8 +86,8 @@ export default (): ProseMirrorExtension => ({
|
|||
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
||||
],
|
||||
nodeViews: {
|
||||
todo_item: (node, view, getPos) => {
|
||||
todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
|
||||
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 table from './extension/table'
|
||||
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 type { Command } from 'prosemirror-state'
|
||||
import placeholder from './extension/placeholder'
|
||||
import todoList from './extension/todo-list'
|
||||
import strikethrough from './extension/strikethrough'
|
||||
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 => ({
|
||||
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 { debounce } from 'lodash'
|
||||
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 { serialize, createMarkdownParser } from '../markdown'
|
||||
import db from '../db'
|
||||
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
||||
|
||||
const isText = (x) => x && x.doc && x.selection
|
||||
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.files)
|
||||
const isFile = (x): boolean => x && (x.text || x.path)
|
||||
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
|
||||
const isDraft = (x): boolean => x && (x.text || x.path)
|
||||
|
||||
export const createCtrl = (initial: State): [Store<State>, any] => {
|
||||
const [store, setState] = createStore(initial)
|
||||
|
||||
const onDiscard = () => {
|
||||
discard()
|
||||
return true
|
||||
}
|
||||
|
||||
const onToggleMarkdown = () => toggleMarkdown()
|
||||
|
||||
const onUndo = () => {
|
||||
if (!isInitialized(store.text)) return
|
||||
const text = store.text as EditorState
|
||||
|
@ -34,56 +27,112 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
} else {
|
||||
undo(text, store.editorView.dispatch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const onRedo = () => {
|
||||
if (!isInitialized(store.text)) return
|
||||
if (!isInitialized(store.text)) return false
|
||||
const text = store.text as EditorState
|
||||
if (store.collab?.started) {
|
||||
yRedo(text)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
const keymap = {
|
||||
[`${mod}-w`]: onDiscard,
|
||||
[`${mod}-w`]: discard,
|
||||
[`${mod}-z`]: onUndo,
|
||||
[`Shift-${mod}-z`]: 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 extensions = createExtensions({
|
||||
config: state.config,
|
||||
markdown: file.markdown,
|
||||
path: file.path,
|
||||
markdown: draft.markdown,
|
||||
path: draft.path,
|
||||
keymap
|
||||
})
|
||||
|
||||
return {
|
||||
text: file.text,
|
||||
text: draft.text,
|
||||
extensions,
|
||||
lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
|
||||
path: file.path,
|
||||
markdown: file.markdown
|
||||
lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined,
|
||||
path: draft.path,
|
||||
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()
|
||||
return [
|
||||
...files,
|
||||
...drafts,
|
||||
{
|
||||
text,
|
||||
lastModified: prev.lastModified?.toISOString(),
|
||||
lastModified: prev.lastModified,
|
||||
path: prev.path,
|
||||
markdown: prev.markdown
|
||||
}
|
||||
|
@ -92,12 +141,12 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
|
||||
const discardText = async () => {
|
||||
const state = unwrap(store)
|
||||
const index = state.files.length - 1
|
||||
const file = index !== -1 ? state.files[index] : undefined
|
||||
const index = state.drafts.length - 1
|
||||
const draft = index !== -1 ? state.drafts[index] : undefined
|
||||
|
||||
let next: Partial<State>
|
||||
if (file) {
|
||||
next = await createTextFromFile(file)
|
||||
if (draft) {
|
||||
next = await createTextFromDraft(draft)
|
||||
} else {
|
||||
const extensions = createExtensions({
|
||||
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({
|
||||
files,
|
||||
drafts,
|
||||
...next,
|
||||
collab: file ? undefined : state.collab,
|
||||
collab: draft ? undefined : state.collab,
|
||||
error: undefined
|
||||
})
|
||||
}
|
||||
|
@ -171,14 +220,14 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
newst.lastModified = new Date(newst.lastModified)
|
||||
}
|
||||
|
||||
for (const file of parsed.files) {
|
||||
if (!isFile(file)) {
|
||||
throw new ServiceError('invalid_file', file)
|
||||
for (const draft of parsed.drafts || []) {
|
||||
if (!isDraft(draft)) {
|
||||
throw new ServiceError('invalid_draft', draft)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isState(newst)) {
|
||||
throw new ServiceError('invalid_state', newState)
|
||||
throw new ServiceError('invalid_state', newst)
|
||||
}
|
||||
|
||||
return newst
|
||||
|
@ -190,7 +239,7 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
setState({
|
||||
...newState(),
|
||||
loading: 'initialized',
|
||||
files: [],
|
||||
drafts: [],
|
||||
fullscreen: store.fullscreen,
|
||||
lastModified: new Date(),
|
||||
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 () => {
|
||||
let data = await fetchData()
|
||||
try {
|
||||
|
@ -235,9 +273,9 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
}
|
||||
|
||||
const saveState = () => debounce(async (state: State) => {
|
||||
const data: any = {
|
||||
const data: State = {
|
||||
lastModified: state.lastModified,
|
||||
files: state.files,
|
||||
drafts: state.drafts,
|
||||
config: state.config,
|
||||
path: state.path,
|
||||
markdown: state.markdown,
|
||||
|
@ -283,14 +321,14 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
|
||||
let newst = state
|
||||
if ((backup && !isEmpty(state.text)) || state.path) {
|
||||
let files = state.files
|
||||
let drafts = state.drafts
|
||||
if (!state.error) {
|
||||
files = addToFiles(files, state)
|
||||
drafts = addToDrafts(drafts, state)
|
||||
}
|
||||
|
||||
newst = {
|
||||
...state,
|
||||
files,
|
||||
drafts,
|
||||
lastModified: undefined,
|
||||
path: undefined,
|
||||
error: undefined
|
||||
|
@ -317,53 +355,6 @@ export const createCtrl = (initial: State): [Store<State>, any] => {
|
|||
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 state = unwrap(store)
|
||||
const extensions = createExtensions({
|
||||
|
|
|
@ -5,10 +5,22 @@ import type { WebrtcProvider } from 'y-webrtc'
|
|||
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||
import type { Command, EditorState } from 'prosemirror-state'
|
||||
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 {
|
||||
cwd?: string;
|
||||
file?: string;
|
||||
draft?: string;
|
||||
room?: string;
|
||||
text?: any;
|
||||
}
|
||||
|
@ -56,11 +68,11 @@ export interface State {
|
|||
extensions?: ProseMirrorExtension[];
|
||||
markdown?: boolean;
|
||||
lastModified?: Date;
|
||||
files: File[];
|
||||
drafts: Draft[];
|
||||
config: Config;
|
||||
error?: ErrorObject;
|
||||
loading: LoadingType;
|
||||
fullscreen: boolean;
|
||||
loading?: LoadingType;
|
||||
fullscreen?: boolean;
|
||||
collab?: Collab;
|
||||
path?: string;
|
||||
args?: Args;
|
||||
|
@ -76,13 +88,6 @@ export interface Draft {
|
|||
extensions?: ProseMirrorExtension[]
|
||||
}
|
||||
|
||||
export interface File {
|
||||
text?: { [key: string]: any };
|
||||
lastModified?: string;
|
||||
path?: string;
|
||||
markdown?: boolean;
|
||||
}
|
||||
|
||||
export class ServiceError extends Error {
|
||||
public errorObject: ErrorObject
|
||||
constructor(id: string, props: unknown) {
|
||||
|
@ -97,7 +102,7 @@ export const useState = () => useContext(StateContext)
|
|||
|
||||
export const newState = (props: Partial<State> = {}): State => ({
|
||||
extensions: [],
|
||||
files: [],
|
||||
drafts: [],
|
||||
loading: 'loading',
|
||||
fullscreen: false,
|
||||
markdown: false,
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.file {
|
||||
&.draft {
|
||||
color: rgba(255,255,255,0.5);
|
||||
line-height: 1.4;
|
||||
margin: 0 0 1em 1.5em;
|
||||
|
|
Loading…
Reference in New Issue
Block a user