refactored and linted

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

View File

@ -12,8 +12,8 @@ export default () => {
<Match when={store.error.id === 'invalid_config'}>
<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>
)

View File

@ -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}>Советы и&nbsp;предложения</span>
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
<span class='sidebar-opener' onClick={toggleSidebar}>Советы и&nbsp;предложения</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' : ''}>

View File

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

View File

@ -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) => [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { Plugin } from 'prosemirror-state'
import { Plugin, Transaction } from 'prosemirror-state'
import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
import { 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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View File

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