diff --git a/package.json b/package.json index 74501095..ddf74b40 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "markdown-it-mark": "^3.0.1", "markdown-it-replace-link": "^1.1.0", "nanostores": "^0.7.0", + "orderedmap": "^2.1.0", "postcss": "^8.4.16", "postcss-modules": "^5.0.0", "prettier": "^2.7.1", diff --git a/src/assets/handle.svg b/src/assets/handle.svg new file mode 100644 index 00000000..b5d94945 --- /dev/null +++ b/src/assets/handle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Editor/components/Editor.tsx b/src/components/Editor/components/Editor.tsx index c87915d3..a19dfc1e 100644 --- a/src/components/Editor/components/Editor.tsx +++ b/src/components/Editor/components/Editor.tsx @@ -20,7 +20,8 @@ export default () => { } return ( { const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() }) return ( ({ marks: plainSchema.spec.marks } : { - nodes: (markdownSchema.spec.nodes as any).update('blockquote', blockquoteSchema), + nodes: (markdownSchema.spec.nodes as OrderedMap).update( + 'blockquote', + blockquoteSchema as unknown as NodeSpec + ), marks: markdownSchema.spec.marks }, plugins: (prev, schema) => [ diff --git a/src/components/Editor/prosemirror/extension/collab.ts b/src/components/Editor/prosemirror/extension/collab.ts index 47455270..2869c883 100644 --- a/src/components/Editor/prosemirror/extension/collab.ts +++ b/src/components/Editor/prosemirror/extension/collab.ts @@ -2,7 +2,13 @@ import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror' import type { YOptions } from '../../store' import type { ProseMirrorExtension } from '../helpers' -export const cursorBuilder = (user: any): HTMLElement => { +export interface EditingProps { + name: string + foreground: string + background: string +} + +export const cursorBuilder = (user: EditingProps): HTMLElement => { const cursor = document.createElement('span') cursor.classList.add('ProseMirror-yjs-cursor') cursor.setAttribute('style', `border-color: ${user.background}`) diff --git a/src/components/Editor/prosemirror/extension/drag-handle.ts b/src/components/Editor/prosemirror/extension/drag-handle.ts index 8374b7c7..0cccab88 100644 --- a/src/components/Editor/prosemirror/extension/drag-handle.ts +++ b/src/components/Editor/prosemirror/extension/drag-handle.ts @@ -1,11 +1,7 @@ import { Plugin, NodeSelection } from 'prosemirror-state' import { DecorationSet, Decoration } from 'prosemirror-view' import type { ProseMirrorExtension } from '../helpers' - -const handleIcon = ` - - - ` +import handleIcon from '../../../../assets/handle.svg' const createDragHandle = () => { const handle = document.createElement('span') diff --git a/src/components/Editor/prosemirror/extension/image.ts b/src/components/Editor/prosemirror/extension/image.ts index aa658089..3d8db5b2 100644 --- a/src/components/Editor/prosemirror/extension/image.ts +++ b/src/components/Editor/prosemirror/extension/image.ts @@ -1,7 +1,8 @@ import { Plugin } from 'prosemirror-state' -import type { Node, Schema } from 'prosemirror-model' +import type { Node, NodeSpec, Schema } from 'prosemirror-model' import type { EditorView } from 'prosemirror-view' -import type { ProseMirrorExtension } from '../helpers' +import type { NodeViewFn, ProseMirrorExtension } from '../helpers' +import type OrderedMap from 'orderedmap' const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/ const MAX_MATCH = 500 @@ -17,7 +18,7 @@ const isUrl = (str: string) => { const isBlank = (text: string) => text === ' ' || text === '\u00A0' -const imageInput = (schema: Schema, path?: string) => +const imageInput = (schema: Schema, _path?: string) => new Plugin({ props: { handleTextInput(view, from, to, text) { @@ -68,7 +69,7 @@ const imageSchema = { src: dom.getAttribute('src'), title: dom.getAttribute('title'), alt: dom.getAttribute('alt'), - path: (dom as any).dataset.path + path: (dom as NodeSpec).dataset.path }) } ], @@ -101,12 +102,12 @@ class ImageView { contentDOM: Element container: HTMLElement handle: HTMLElement - onResizeFn: any - onResizeEndFn: any + onResizeFn: (e: Event) => void + onResizeEndFn: (e: Event) => void width: number updating: number - constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, path: string) { + constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, _path: string) { this.node = node this.view = view this.getPos = getPos @@ -161,12 +162,12 @@ class ImageView { export default (path?: string): ProseMirrorExtension => ({ schema: (prev) => ({ ...prev, - nodes: (prev.nodes as any).update('image', imageSchema) + nodes: (prev.nodes as OrderedMap).update('image', imageSchema as unknown as NodeSpec) }), plugins: (prev, schema) => [...prev, imageInput(schema, path)], nodeViews: { image: (node, view, getPos) => { return new ImageView(node, view, getPos, view.state.schema, path) } - } as any + } as unknown as { [key: string]: NodeViewFn } }) diff --git a/src/components/Editor/prosemirror/extension/link.ts b/src/components/Editor/prosemirror/extension/link.ts index af1afdb5..6a91fcae 100644 --- a/src/components/Editor/prosemirror/extension/link.ts +++ b/src/components/Editor/prosemirror/extension/link.ts @@ -31,7 +31,6 @@ const markdownLinks = (schema: Schema) => if (action?.pos) { (state as any).pos = action.pos } - return state } }, diff --git a/src/components/Editor/prosemirror/extension/menu.ts b/src/components/Editor/prosemirror/extension/menu.ts index 293e7e32..d2a1ae4d 100644 --- a/src/components/Editor/prosemirror/extension/menu.ts +++ b/src/components/Editor/prosemirror/extension/menu.ts @@ -16,48 +16,34 @@ import { import type { MenuItemSpec, MenuElement } from 'prosemirror-menu' import { wrapInList } from 'prosemirror-schema-list' -import { NodeSelection } from 'prosemirror-state' +import { Command, EditorState, NodeSelection, Transaction } from 'prosemirror-state' import { TextField, openPrompt } from './prompt' import type { ProseMirrorExtension } from '../helpers' -import type { Schema } from 'prosemirror-model' +import type { Attrs, MarkType, NodeType, Schema } from 'prosemirror-model' +import type { EditorView } from 'prosemirror-view' // Helpers to create specific types of items -function canInsert(state: { selection: { $from: any } }, nodeType: any) { +function canInsert(state: EditorState, nodeType: NodeType) { const $from = state.selection.$from - for (let d = $from.depth; d >= 0; d--) { const index = $from.index(d) - if ($from.node(d).canReplaceWith(index, index, nodeType)) return true } - return false } -function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) { +function insertImageItem(nodeType: NodeType) { return new MenuItem({ icon: icons.image, label: 'image', - enable(state: any) { + enable(state) { return canInsert(state, nodeType) }, - run( - state: { - selection: { node?: any; from?: any; to?: any } - doc: { textBetween: (arg0: any, arg1: any, arg2: string) => any } - }, - _: any, - view: { - dispatch: (arg0: any) => void - state: { tr: { replaceSelectionWith: (arg0: any) => any } } - focus: () => void - } - ) { - const { from, to, node } = state.selection + run(state: EditorState, _, view: EditorView) { + const { from, to, node } = state.selection as NodeSelection let attrs = null - if (state.selection instanceof NodeSelection && node.type === nodeType) { attrs = node.attrs } @@ -77,7 +63,7 @@ function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) { }) }, // eslint-disable-next-line no-shadow - callback(attrs: any) { + callback(attrs: Attrs) { view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs))) view.focus() } @@ -86,53 +72,31 @@ function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) { }) } -function cmdItem( - cmd: (arg0: any) => any, - options: { [x: string]: any; active?: (state: any) => any; enable?: any; title?: any; select?: any } -) { - const passedOptions = { - label: options.title, - run: cmd - } as { [key: string]: any } - +function cmdItem(cmd: Command, options: MenuItemSpec) { + const passedOptions = { label: options.title, run: cmd } as MenuItemSpec Object.keys(options).forEach((prop) => (passedOptions[prop] = options[prop])) - - if ((!options.enable || options.enable === true) && !options.select) { - passedOptions[options.enable ? 'enable' : 'select'] = (state: any) => cmd(state) - } - + // TODO: enable/disable items logix + passedOptions.select = (state) => cmd(state) return new MenuItem(passedOptions as MenuItemSpec) } -function markActive( - state: { - selection: { from: any; $from: any; to: any; empty: any } - storedMarks: any - doc: { rangeHasMark: (arg0: any, arg1: any, arg2: any) => any } - }, - type: { isInSet: (arg0: any) => any } -) { +function markActive(state: EditorState, type: MarkType) { const { from, $from, to, empty } = state.selection - if (empty) return type.isInSet(state.storedMarks || $from.marks()) - return state.doc.rangeHasMark(from, to, type) } -function markItem(markType: any, options: { [x: string]: any; title?: string; icon?: any }) { +function markItem(markType: MarkType, options: MenuItemSpec) { const passedOptions = { - active(state: any) { + active(state) { return markActive(state, markType) - }, - enable: true - } as { [key: string]: any } - + } + } as MenuItemSpec Object.keys(options).forEach((prop: string) => (passedOptions[prop] = options[prop])) - return cmdItem(toggleMark(markType), passedOptions) } -function linkItem(markType: any) { +function linkItem(markType: MarkType) { return new MenuItem({ title: 'Add or remove link', icon: { @@ -140,19 +104,13 @@ function linkItem(markType: any) { height: 18, path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z' }, - active(state: any) { - return markActive(state, markType) - }, - enable(state: { selection: { empty: any } }) { - return !state.selection.empty - }, - run(state: any, dispatch: any, view: { state: any; dispatch: any; focus: () => void }) { + active: (state) => Boolean(markActive(state, markType)), + enable: (state: EditorState) => !state.selection.empty, + run(state: EditorState, dispatch: (t: Transaction) => void, view: EditorView) { if (markActive(state, markType)) { toggleMark(markType)(state, dispatch) - return true } - openPrompt({ fields: { href: new TextField({ @@ -160,7 +118,7 @@ function linkItem(markType: any) { required: true }) }, - callback(attrs: any) { + callback(attrs: Attrs) { toggleMark(markType, attrs)(view.state, view.dispatch) view.focus() } @@ -169,14 +127,8 @@ function linkItem(markType: any) { }) } -function wrapListItem( - nodeType: any, - options: { - title?: string - icon?: { width: number; height: number; path: string } | { width: number; height: number; path: string } - attrs?: any - } -) { +function wrapListItem(nodeType: NodeType, options: MenuItemSpec & { attrs: Attrs }) { + options.run = (_) => true return cmdItem(wrapInList(nodeType, options.attrs), options) } @@ -255,7 +207,7 @@ type BuildSchema = { */ export function buildMenuItems(schema: Schema) { const r: { [key: string]: MenuItem | MenuItem[] } = {} - let type: any + let type: NodeType | MarkType if ((type = schema.marks.strong)) { r.toggleStrong = markItem(type, { @@ -265,7 +217,7 @@ export function buildMenuItems(schema: Schema) { height: 16, path: 'M9.82857 7.76C10.9371 6.99429 11.7143 5.73714 11.7143 4.57143C11.7143 1.98857 9.71428 0 7.14286 0H0V16H8.04571C10.4343 16 12.2857 14.0571 12.2857 11.6686C12.2857 9.93143 11.3029 8.44571 9.82857 7.76ZM3.42799 2.85708H6.85656C7.80513 2.85708 8.57085 3.6228 8.57085 4.57137C8.57085 5.51994 7.80513 6.28565 6.85656 6.28565H3.42799V2.85708ZM3.42799 13.1429H7.42799C8.37656 13.1429 9.14228 12.3772 9.14228 11.4286C9.14228 10.4801 8.37656 9.71434 7.42799 9.71434H3.42799V13.1429Z' } - }) + } as MenuItemSpec) } if ((type = schema.marks.em)) { @@ -276,14 +228,14 @@ export function buildMenuItems(schema: Schema) { height: 16, path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z' } - }) + } as MenuItemSpec) } if ((type = schema.marks.code)) { r.toggleCode = markItem(type, { title: 'Toggle code font', icon: icons.code - }) + } as MenuItemSpec) } if ((type = schema.marks.link)) r.toggleLink = linkItem(type) @@ -298,7 +250,7 @@ export function buildMenuItems(schema: Schema) { height: 16, path: 'M0.000114441 1.6C0.000114441 0.714665 0.71478 0 1.60011 0C2.48544 0 3.20011 0.714665 3.20011 1.6C3.20011 2.48533 2.48544 3.19999 1.60011 3.19999C0.71478 3.19999 0.000114441 2.48533 0.000114441 1.6ZM0 8.00013C0 7.1148 0.714665 6.40014 1.6 6.40014C2.48533 6.40014 3.19999 7.1148 3.19999 8.00013C3.19999 8.88547 2.48533 9.60013 1.6 9.60013C0.714665 9.60013 0 8.88547 0 8.00013ZM1.6 12.8C0.714665 12.8 0 13.5254 0 14.4C0 15.2747 0.725332 16 1.6 16C2.47466 16 3.19999 15.2747 3.19999 14.4C3.19999 13.5254 2.48533 12.8 1.6 12.8ZM19.7333 15.4662H4.79999V13.3329H19.7333V15.4662ZM4.79999 9.06677H19.7333V6.93344H4.79999V9.06677ZM4.79999 2.66664V0.533307H19.7333V2.66664H4.79999Z' } - }) + } as MenuItemSpec & { attrs: Attrs }) } if ((type = schema.nodes.ordered_list)) { @@ -309,7 +261,7 @@ export function buildMenuItems(schema: Schema) { height: 16, path: 'M2.00002 4.00003H1.00001V1.00001H0V0H2.00002V4.00003ZM2.00002 13.5V13H0V12H3.00003V16H0V15H2.00002V14.5H1.00001V13.5H2.00002ZM0 6.99998H1.80002L0 9.1V10H3.00003V9H1.20001L3.00003 6.89998V5.99998H0V6.99998ZM4.9987 2.99967V0.999648H18.9988V2.99967H4.9987ZM4.9987 15.0001H18.9988V13.0001H4.9987V15.0001ZM18.9988 8.99987H4.9987V6.99986H18.9988V8.99987Z' } - }) + } as MenuItemSpec & { attrs: Attrs }) } if ((type = schema.nodes.blockquote)) { @@ -360,10 +312,8 @@ export function buildMenuItems(schema: Schema) { r.insertHorizontalRule = new MenuItem({ label: '---', icon: icons.horizontal_rule, - enable(state: any) { - return canInsert(state, hr) - }, - run(state: { tr: { replaceSelectionWith: (arg0: any) => any } }, dispatch: (arg0: any) => void) { + enable: (state) => canInsert(state, hr), + run(state: EditorState, dispatch: (tr: Transaction) => void) { dispatch(state.tr.replaceSelectionWith(hr.create())) } }) @@ -404,7 +354,7 @@ export default (): ProseMirrorExtension => ({ ...prev, menuBar({ floating: true, - content: buildMenuItems(schema).fullMenu as any[] + content: buildMenuItems(schema).fullMenu as MenuItem | MenuItem[] }) ] }) diff --git a/src/components/Editor/prosemirror/extension/paste-markdown.ts b/src/components/Editor/prosemirror/extension/paste-markdown.ts index c8d6beb9..465dc82a 100644 --- a/src/components/Editor/prosemirror/extension/paste-markdown.ts +++ b/src/components/Editor/prosemirror/extension/paste-markdown.ts @@ -64,7 +64,7 @@ const pasteMarkdown = (schema: Schema) => { event.preventDefault() const paste = parser.parse(text) - const slice = paste as any + const slice = paste as Node & { openStart: number; openEnd: number } const fragment = shiftKey ? slice.content : transform(schema, slice.content) const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd)) diff --git a/src/components/Editor/prosemirror/extension/prompt.ts b/src/components/Editor/prosemirror/extension/prompt.ts index 1cb4dea1..446452f0 100644 --- a/src/components/Editor/prosemirror/extension/prompt.ts +++ b/src/components/Editor/prosemirror/extension/prompt.ts @@ -1,12 +1,11 @@ const prefix = 'ProseMirror-prompt' // eslint-disable-next-line sonarjs/cognitive-complexity -export function openPrompt(options: any) { +export function openPrompt(options) { const wrapper = document.body.appendChild(document.createElement('div')) wrapper.className = prefix - - const mouseOutside = (e: any) => { - if (!wrapper.contains(e.target)) close() + const mouseOutside = (e: MouseEvent) => { + if (!wrapper.contains(e.target as Node)) close() } setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50) const close = () => { @@ -14,7 +13,7 @@ export function openPrompt(options: any) { if (wrapper.parentNode) wrapper.remove() } - const domFields: any = [] + const domFields = [] options.fields.forEach((name) => { domFields.push(options.fields[name].render()) }) @@ -33,7 +32,7 @@ export function openPrompt(options: any) { if (options.title) { form.appendChild(document.createElement('h5')).textContent = options.title } - domFields.forEach((field: any) => { + domFields.forEach((field) => { form.appendChild(document.createElement('div')).appendChild(field) }) const buttons = form.appendChild(document.createElement('div')) @@ -74,11 +73,11 @@ export function openPrompt(options: any) { } }) - const input: any = form.elements[0] - if (input) input.focus() + const inpel = form.elements[0] as HTMLInputElement + if (inpel) inpel.focus() } -function getValues(fields: any, domFields: any) { +function getValues(fields, domFields) { const result = Object.create(null) let i = 0 fields.forEarch((name) => { @@ -95,7 +94,7 @@ function getValues(fields: any, domFields: any) { return result } -function reportInvalid(dom: any, message: any) { +function reportInvalid(dom: HTMLElement, message: string) { const parent = dom.parentNode const msg = parent.appendChild(document.createElement('div')) msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px' @@ -106,13 +105,24 @@ function reportInvalid(dom: any, message: any) { setTimeout(() => parent.removeChild(msg), 1500) } +interface FieldOptions { + options: { value: string; label: string }[] + required: boolean + label: string + value: string + validateType: (v) => boolean + validate: (v) => boolean + read: (v) => string + clean: (v) => boolean +} + export class Field { - options: any - constructor(options: any) { + options: FieldOptions + constructor(options) { this.options = options } - read(dom: any) { + read(dom) { return dom.value } // :: (any) → ?string @@ -121,13 +131,12 @@ export class Field { return typeof _value === typeof '' } - validate(value: any) { + validate(value) { if (!value && this.options.required) return 'Required field' - return this.validateType(value) || (this.options.validate && this.options.validate(value)) } - clean(value: any) { + clean(value) { return this.options.clean ? this.options.clean(value) : value } } @@ -147,7 +156,7 @@ export class TextField extends Field { export class SelectField extends Field { render() { const select = document.createElement('select') - this.options.options.forEach((o: { value: string; label: string }) => { + this.options.options.forEach((o) => { const opt = select.appendChild(document.createElement('option')) opt.value = o.value opt.selected = o.value === this.options.value diff --git a/src/components/Editor/prosemirror/extension/selection.ts b/src/components/Editor/prosemirror/extension/selection.ts index 235bc5b6..61300ad0 100644 --- a/src/components/Editor/prosemirror/extension/selection.ts +++ b/src/components/Editor/prosemirror/extension/selection.ts @@ -1,21 +1,22 @@ import { renderGrouped } from 'prosemirror-menu' -import { Plugin } from 'prosemirror-state' +import { EditorState, Plugin } from 'prosemirror-state' +import type { EditorView } from 'prosemirror-view' import type { ProseMirrorExtension } from '../helpers' import { buildMenuItems } from './menu' export class SelectionTooltip { - tooltip: any + tooltip: HTMLElement - constructor(view: any, schema: any) { + constructor(view: EditorView, schema) { this.tooltip = document.createElement('div') this.tooltip.className = 'tooltip' view.dom.parentNode.appendChild(this.tooltip) - const { dom } = renderGrouped(view, (buildMenuItems(schema) as any).fullMenu) + const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any) this.tooltip.appendChild(dom) this.update(view, null) } - update(view: any, lastState: any) { + update(view: EditorView, lastState: EditorState) { const state = view.state if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) { return @@ -41,9 +42,9 @@ export class SelectionTooltip { } } -export function toolTip(schema: any) { +export function toolTip(schema) { return new Plugin({ - view(editorView: any) { + view(editorView: EditorView) { return new SelectionTooltip(editorView, schema) } }) diff --git a/src/components/Editor/prosemirror/extension/table.ts b/src/components/Editor/prosemirror/extension/table.ts index 8fdba064..d41dcb3c 100644 --- a/src/components/Editor/prosemirror/extension/table.ts +++ b/src/components/Editor/prosemirror/extension/table.ts @@ -1,8 +1,9 @@ import { EditorState, Selection } from 'prosemirror-state' -import type { Node, Schema, ResolvedPos } from 'prosemirror-model' +import type { Node, Schema, ResolvedPos, NodeSpec } from 'prosemirror-model' import { InputRule, inputRules } from 'prosemirror-inputrules' import { keymap } from 'prosemirror-keymap' import type { ProseMirrorExtension } from '../helpers' +import type OrderedMap from 'orderedmap' export const tableInputRule = (schema: Schema) => new InputRule( @@ -174,7 +175,7 @@ const getTextSize = (n: Node) => { export default (): ProseMirrorExtension => ({ schema: (prev) => ({ ...prev, - nodes: (prev.nodes as any).append(tableSchema) + nodes: (prev.nodes as OrderedMap).append(tableSchema as NodeSpec) }), // eslint-disable-next-line sonarjs/cognitive-complexity plugins: (prev, schema) => [ diff --git a/src/components/Editor/prosemirror/setup.ts b/src/components/Editor/prosemirror/setup.ts index afca1ff7..8b4067d8 100644 --- a/src/components/Editor/prosemirror/setup.ts +++ b/src/components/Editor/prosemirror/setup.ts @@ -1,80 +1,73 @@ -import { keymap } from 'prosemirror-keymap' -import type { ProseMirrorExtension } from './helpers' -import { Schema } from 'prosemirror-model' -import base from './extension/base' -import markdown from './extension/markdown' -import link from './extension/link' -// import scroll from './prosemirror/extension/scroll' -import todoList from './extension/todo-list' -import code from './extension/code' -import strikethrough from './extension/strikethrough' -import placeholder from './extension/placeholder' // import menu from './extension/menu' -import image from './extension/image' -import dragHandle from './extension/drag-handle' -import pasteMarkdown from './extension/paste-markdown' -import table from './extension/table' +// import scroll from './prosemirror/extension/scroll' +import { keymap } from 'prosemirror-keymap' +import { Schema } from 'prosemirror-model' +import type { Command } from 'prosemirror-state' +import { t } from '../../../utils/intl' +import base from './extension/base' +import code from './extension/code' import collab from './extension/collab' -import type { Config, YOptions } from '../store' +import dragHandle from './extension/drag-handle' +import image from './extension/image' +import link from './extension/link' +import markdown from './extension/markdown' +import pasteMarkdown from './extension/paste-markdown' +import placeholder from './extension/placeholder' import selectionMenu from './extension/selection' +import strikethrough from './extension/strikethrough' +import table from './extension/table' +import todoList from './extension/todo-list' +import type { Config, YOptions } from '../store' +import type { ProseMirrorExtension } from './helpers' -interface Props { +interface ExtensionsProps { data?: unknown - keymap?: any + keymap?: { [key: string]: Command } config: Config markdown: boolean path?: string y?: YOptions schema?: Schema + collab?: boolean } -const customKeymap = (props: Props): ProseMirrorExtension => ({ +const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({ plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev) }) -/* -const codeMirrorKeymap = (props: Props) => { - const keys = [] - for (const key in props.keymap) { - keys.push({key: key, run: props.keymap[key]}) - } - return cmKeymap.of(keys) +export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => { + const eee = [ + // scroll(props.config.typewriterMode), + placeholder(t('Just start typing...')), + customKeymap(props), + base(props.markdown), + selectionMenu() + ] + if (props.markdown) { + eee.push( + markdown(), + todoList(), + dragHandle(), + code(), + strikethrough(), + link(), + table(), + image(props.path), + pasteMarkdown() + /* + codeBlock({ + theme: codeTheme(props.config), + typewriterMode: props.config.typewriterMode, + fontSize: props.config.fontSize, + prettier: props.config.prettier, + extensions: () => [codeMirrorKeymap(props)], + }), + */ + ) + } + if (props.collab) eee.push(collab(props.y)) + return eee } -*/ -export const createExtensions = (props: Props): ProseMirrorExtension[] => - props.markdown - ? [ - placeholder('Просто начните...'), - customKeymap(props), - base(props.markdown), - collab(props.y), - selectionMenu() - ] - : [ - selectionMenu(), - customKeymap(props), - base(props.markdown), - markdown(), - todoList(), - dragHandle(), - code(), - strikethrough(), - link(), - table(), - image(props.path), - pasteMarkdown(), - collab(props.y) - // scroll(props.config.typewriterMode), - /* - codeBlock({ - theme: codeTheme(props.config), - typewriterMode: props.config.typewriterMode, - fontSize: props.config.fontSize, - prettier: props.config.prettier, - extensions: () => [codeMirrorKeymap(props)], - }), - */ - ] export const createEmptyText = () => ({ doc: { @@ -88,7 +81,7 @@ export const createEmptyText = () => ({ } }) -export const createSchema = (props: Props) => { +export const createSchema = (props: ExtensionsProps) => { const extensions = createExtensions({ config: props.config, markdown: props.markdown, diff --git a/src/components/Editor/store/ctrl.ts b/src/components/Editor/store/ctrl.ts index 1453cf45..60570602 100644 --- a/src/components/Editor/store/ctrl.ts +++ b/src/components/Editor/store/ctrl.ts @@ -1,6 +1,6 @@ import { Store, createStore, unwrap } from 'solid-js/store' import { v4 as uuidv4 } from 'uuid' -import type { EditorState } from 'prosemirror-state' +import type { Command, EditorState } from 'prosemirror-state' import { undo, redo } from 'prosemirror-history' import { selectAll, deleteSelection } from 'prosemirror-commands' import * as Y from 'yjs' @@ -23,7 +23,7 @@ const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.dra const isDraft = (x): boolean => x && (x.text || x.path) const mod = 'Ctrl' -export const createCtrl = (initial): [Store, any] => { +export const createCtrl = (initial): [Store, { [key: string]: any }] => { const [store, setState] = createStore(initial) const onNew = () => { @@ -64,7 +64,7 @@ export const createCtrl = (initial): [Store, any] => { [`Shift-${mod}-z`]: onRedo, [`${mod}-y`]: onRedo, [`${mod}-m`]: onToggleMarkdown - } + } as { [key: string]: Command } const createTextFromDraft = async (d: Draft): Promise => { let draft = d @@ -83,7 +83,7 @@ export const createCtrl = (initial): [Store, any] => { return { text: draft.text, extensions, - updatedAt: draft.updatedAt ? new Date(draft.updatedAt) : undefined, + lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined, path: draft.path, markdown: draft.markdown } @@ -96,7 +96,7 @@ export const createCtrl = (initial): [Store, any] => { ...drafts, { body: text, - updatedAt: prev.updatedAt as Date, + lastModified: prev.lastModified as Date, path: prev.path, markdown: prev.markdown } as Draft @@ -121,7 +121,7 @@ export const createCtrl = (initial): [Store, any] => { next = { text: createEmptyText(), extensions, - updatedAt: new Date(), + lastModified: new Date(), path: undefined, markdown: state.markdown } @@ -227,7 +227,7 @@ export const createCtrl = (initial): [Store, any] => { } else if (data.args.text) { data = await doOpenDraft(data, { text: { ...JSON.parse(data.args.text) }, - updatedAt: new Date() + lastModified: new Date() }) } else if (data.args.draft) { const draft = await loadDraft(data.config, data.args.draft) @@ -258,7 +258,7 @@ export const createCtrl = (initial): [Store, any] => { const loadDraft = async (config: Config, path: string): Promise => { const draftstore = useStore(draftsatom) const draft = createMemo(() => draftstore()[path]) - const lastModified = draft().updatedAt + const lastModified = draft().lastModified const draftContent = draft().body const schema = createSchema({ config, @@ -280,7 +280,7 @@ export const createCtrl = (initial): [Store, any] => { ...draft(), body: doc, text, - updatedAt: lastModified.toISOString(), + lastModified: lastModified.toISOString(), path } } @@ -327,9 +327,9 @@ export const createCtrl = (initial): [Store, any] => { const item = index === -1 ? draft : state.drafts[index] let drafts = state.drafts.filter((f) => f !== item) if (!isEmpty(state.text as EditorState) && state.lastModified) { - drafts = addToDrafts(drafts, { updatedAt: new Date(), text: state.text } as Draft) + drafts = addToDrafts(drafts, { lastModified: new Date(), text: state.text } as Draft) } - draft.updatedAt = item.updatedAt + draft.lastModified = item.lastModified const next = await createTextFromDraft(draft) return { @@ -343,7 +343,8 @@ export const createCtrl = (initial): [Store, any] => { const saveState = () => debounce(async (state: State) => { - const data: any = { + const data: State = { + loading: 'initialized', lastModified: state.lastModified, drafts: state.drafts, config: state.config, @@ -357,7 +358,8 @@ export const createCtrl = (initial): [Store, any] => { if (isInitialized(state.text as EditorState)) { if (state.path) { const text = serialize(store.editorView.state) - // TODO: await remote.writeDraft(state.path, text) + // await remote.writeDraft(state.path, text) + draftsatom.setKey(state.path, text) } else { data.text = store.editorView.state.toJSON() } @@ -418,7 +420,7 @@ export const createCtrl = (initial): [Store, any] => { if ((backup && !isEmpty(state.text as EditorState)) || state.path) { let drafts = state.drafts if (!state.error) { - drafts = addToDrafts(drafts, { updatedAt: new Date(), text: state.text } as Draft) + drafts = addToDrafts(drafts, { lastModified: new Date(), text: state.text } as Draft) } newst = { @@ -455,7 +457,7 @@ export const createCtrl = (initial): [Store, any] => { const editorState = store.text as EditorState const markdown = !state.markdown const selection = { type: 'text', anchor: 1, head: 1 } - let doc: any + let doc if (markdown) { const lines = serialize(editorState).split('\n') @@ -495,6 +497,7 @@ export const createCtrl = (initial): [Store, any] => { extensions, markdown }) + return true } const updateConfig = (config: Partial) => { diff --git a/src/components/Editor/store/index.ts b/src/components/Editor/store/index.ts index 66f3f72b..284055cf 100644 --- a/src/components/Editor/store/index.ts +++ b/src/components/Editor/store/index.ts @@ -5,7 +5,6 @@ import type { WebrtcProvider } from 'y-webrtc' import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers' import type { EditorView } from 'prosemirror-view' import { createEmptyText } from '../prosemirror/setup' -import type { Shout } from '../../../graphql/types.gen' export interface Args { draft: string // path to draft @@ -70,7 +69,7 @@ export interface State { export interface Draft { extensions?: ProseMirrorExtension[] - updatedAt: Date + lastModified: Date body?: string text?: { doc: any; selection: { type: string; anchor: number; head: number } } path?: string diff --git a/src/components/Editor/styles/Editor.scss b/src/components/Editor/styles/Editor.scss index a8bdd9ae..90f27531 100644 --- a/src/components/Editor/styles/Editor.scss +++ b/src/components/Editor/styles/Editor.scss @@ -2,48 +2,64 @@ @import './Sidebar'; .editor { - flex: 1; - padding-top: 1em; + margin: 0.5em; + padding: 1em; + min-width: 50%; + min-height: fit-content; + display: inline-block; + border: 1px dotted rgb(0 0 0 / 80%); +} - label { - display: block; - } +a { + color: rgb(0 100 200); + text-decoration: none; +} - input, - button, - select, - textarea { - font-family: inherit; - font-size: inherit; - -webkit-padding: 0.4em 0; - padding: 0.4em; - margin: 0 0 0.5em; - box-sizing: border-box; - border: 1px solid #ccc; - border-radius: 2px; - } +a:hover { + text-decoration: underline; +} - input:disabled { - color: #ccc; - } +a:visited { + color: rgb(0 100 200 / 70%); +} - button { - color: #333; - background-color: #f4f4f4; - outline: none; - } +label { + display: block; +} - button:disabled { - color: #999; - } +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + padding: 0.4em; + margin: 0 0 0.5em; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 2px; +} - button:not(:disabled):active { - background-color: #ddd; - } +input:disabled { + color: #ccc; +} - button:focus { - border-color: #666; - } +button { + color: #333; + background-color: #f4f4f4; + outline: none; +} + +button:disabled { + color: #999; +} + +button:not(:disabled):active { + background-color: #ddd; +} + +button:focus { + border-color: #666; } .ProseMirror { @@ -104,17 +120,17 @@ } blockquote { - border-left: 2px solid; @include font-size(1.6rem); margin: 1.5em 0; + border-left: 2px solid; padding-left: 1.6em; } } .ProseMirror-menuitem { - display: flex; font-size: small; + display: flex; &:hover { > * { @@ -175,7 +191,7 @@ content: ''; border-left: 4px solid transparent; border-right: 4px solid transparent; - border-top: 4px solid currentcolor; + border-top: 4px solid draftcurrentcolor; opacity: 0.6; position: absolute; right: 4px; @@ -215,7 +231,7 @@ content: ''; border-top: 4px solid transparent; border-bottom: 4px solid transparent; - border-left: 4px solid currentcolor; + border-left: 4px solid draftcurrentcolor; opacity: 0.6; position: absolute; right: 4px; @@ -270,7 +286,7 @@ } .ProseMirror-icon svg { - fill: currentcolor; + fill: draftcurrentcolor; height: 1em; } @@ -333,7 +349,7 @@ li.ProseMirror-selectednode::after { .ProseMirror-prompt { background: #fff; - box-shadow: 0 4px 10px rgb(0 0 0 / 25%); + box-shadow: 0 4px 10px rgba(0 0 0 / 25%); font-size: 0.7em; position: absolute; } @@ -378,7 +394,7 @@ li.ProseMirror-selectednode::after { .tooltip { background: var(--background); - box-shadow: 0 4px 10px rgb(0 0 0 / 25%); + box-shadow: 0 4px 10px rgba(0 0 0 / 25%); color: #000; display: flex; position: absolute; diff --git a/src/components/Editor/styles/Sidebar.scss b/src/components/Editor/styles/Sidebar.scss index 86e6d67f..4e655133 100644 --- a/src/components/Editor/styles/Sidebar.scss +++ b/src/components/Editor/styles/Sidebar.scss @@ -92,10 +92,11 @@ } .sidebar-container { - color: rgb(255 255 255 / 50%); - font-family: Muller; @include font-size(1.6rem); + color: rgb(255 255 255 / 50%); + font-family: Muller; + display: inline-flex; overflow: hidden; position: relative; top: 0; diff --git a/src/stores/editor.ts b/src/stores/editor.ts index 34e4a161..015aa0de 100644 --- a/src/stores/editor.ts +++ b/src/stores/editor.ts @@ -1,4 +1,4 @@ -import { persistentAtom } from '@nanostores/persistent' +import { persistentAtom, persistentMap } from '@nanostores/persistent' import type { Reaction } from '../graphql/types.gen' import { atom } from 'nanostores' import { createSignal } from 'solid-js' @@ -18,7 +18,7 @@ interface Collab { title?: string } -export const drafts = persistentAtom<{ [key: string]: Draft }>( +export const drafts = persistentMap<{ [key: string]: Draft }>( 'drafts', {}, { diff --git a/yarn.lock b/yarn.lock index a7239546..a77d0a7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8553,7 +8553,7 @@ ora@^6.1.0: strip-ansi "^7.0.1" wcwidth "^1.0.1" -orderedmap@^2.0.0: +orderedmap@^2.0.0, orderedmap@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-2.1.0.tgz#819457082fa3a06abd316d83a281a1ca467437cd" integrity sha512-/pIFexOm6S70EPdznemIz3BQZoJ4VTFrhqzu0ACBqBgeLsLxq8e6Jim63ImIfwW/zAD1AlXpRMlOv3aghmo4dA==