demo
This commit is contained in:
parent
1d7a71ae3a
commit
642d8b9dd1
|
@ -27,6 +27,7 @@ module.exports = {
|
||||||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import { useState } from '../store/context'
|
import { useState } from '../store/context'
|
||||||
import { ProseMirror } from './ProseMirror'
|
import { ProseMirror } from './ProseMirror'
|
||||||
import '../styles/Editor.scss'
|
import '../styles/Editor.scss'
|
||||||
|
@ -9,17 +9,11 @@ export const Editor = () => {
|
||||||
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
||||||
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
||||||
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
||||||
const style = () => {
|
// const editorCss = (config) => css``
|
||||||
if (store.error) {
|
const style = () => (store.error ? `display: none;` : store.markdown ? `white-space: pre-wrap;` : '')
|
||||||
return `display: none;`
|
|
||||||
} else {
|
|
||||||
return store.markdown ? `white-space: pre-wrap;` : ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<ProseMirror
|
<ProseMirror
|
||||||
// eslint-disable-next-line solid/no-react-specific-props
|
className='editor'
|
||||||
className="editor"
|
|
||||||
style={style()}
|
style={style()}
|
||||||
editorView={store.editorView}
|
editorView={store.editorView}
|
||||||
text={store.text}
|
text={store.text}
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import { Switch, Match } from 'solid-js'
|
import { Switch, Match } from 'solid-js'
|
||||||
|
import { useState } from '../store/context'
|
||||||
import '../styles/Button.scss'
|
import '../styles/Button.scss'
|
||||||
import { ErrorObject, useState } from '../store/context'
|
|
||||||
import { t } from '../../../utils/intl'
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [store] = useState()
|
const [store] = useState()
|
||||||
return (
|
return (
|
||||||
<Switch fallback={<Other />}>
|
<Switch fallback={<Other />}>
|
||||||
<Match when={store.error.id === 'invalid_state'}>
|
<Match when={store.error.id === 'invalid_state'}>
|
||||||
<InvalidState title="Invalid State" />
|
<InvalidState title='Invalid State' />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.error.id === 'invalid_config'}>
|
<Match when={store.error.id === 'invalid_config'}>
|
||||||
<InvalidState title="Invalid Config" />
|
<InvalidState title='Invalid Config' />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={store.error.id === 'invalid_draft'}>
|
<Match when={store.error.id === 'invalid_file'}>
|
||||||
<InvalidState title="Invalid Draft" />
|
<InvalidState title='Invalid File' />
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
@ -25,17 +24,19 @@ const InvalidState = (props: { title: string }) => {
|
||||||
const onClick = () => ctrl.clean()
|
const onClick = () => ctrl.clean()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error">
|
<div class='error'>
|
||||||
<div class="container">
|
<div class='container'>
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
<p>
|
<p>
|
||||||
{t('Editing conflict, please copy your notes and refresh page')}
|
There is an error with the editor state. This is probably due to an old version in which the data
|
||||||
|
structure has changed. Automatic data migrations may be supported in the future. To fix this now,
|
||||||
|
you can copy important notes from below, clean the state and paste it again.
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<pre>
|
||||||
<code>{JSON.stringify(store.error.props)}</code>
|
<code>{JSON.stringify(store.error.props)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class="primary" onClick={onClick}>
|
<button class='primary' onClick={onClick}>
|
||||||
{t('Clean')}
|
Clean
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,18 +48,18 @@ const Other = () => {
|
||||||
const onClick = () => ctrl.discard()
|
const onClick = () => ctrl.discard()
|
||||||
|
|
||||||
const getMessage = () => {
|
const getMessage = () => {
|
||||||
const err = (store.error.props as ErrorObject['props']).error
|
const err = (store.error.props as any).error
|
||||||
return typeof err === 'string' ? err : err.message
|
return typeof err === 'string' ? err : err.message
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error">
|
<div class='error'>
|
||||||
<div class="container">
|
<div class='container'>
|
||||||
<h1>An error occurred.</h1>
|
<h1>An error occurred.</h1>
|
||||||
<pre>
|
<pre>
|
||||||
<code>{getMessage()}</code>
|
<code>{getMessage()}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class="primary" onClick={onClick}>
|
<button class='primary' onClick={onClick}>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
import type { JSX } from 'solid-js/jsx-runtime'
|
import { Config } from '../store/context'
|
||||||
import type { Config } from '../store/context'
|
|
||||||
import '../styles/Layout.scss'
|
import '../styles/Layout.scss'
|
||||||
|
|
||||||
export type Styled = {
|
export type Styled = {
|
||||||
children: JSX.Element
|
children: any;
|
||||||
config?: Config
|
config?: Config;
|
||||||
'data-testid'?: string
|
'data-testid'?: string;
|
||||||
onClick?: () => void
|
onClick?: () => void;
|
||||||
onMouseEnter?: (ev: MouseEvent) => void
|
onMouseEnter?: (e: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Layout = (props: Styled) => {
|
export const Layout = (props: Styled) => {
|
||||||
return (
|
return (<div onMouseEnter={props.onMouseEnter} class='layout' data-testid={props['data-testid']}>
|
||||||
<div onMouseEnter={props.onMouseEnter} class="layout" data-testid={props['data-testid']}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { createEffect, untrack } from 'solid-js'
|
import { createEffect, untrack } from 'solid-js'
|
||||||
import { Store, unwrap } from 'solid-js/store'
|
import { Store, unwrap } from 'solid-js/store'
|
||||||
import { EditorState, EditorStateConfig, Transaction } from 'prosemirror-state'
|
import { EditorState, Transaction } from 'prosemirror-state'
|
||||||
import { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import { Schema } from 'prosemirror-model'
|
import { Schema } from 'prosemirror-model'
|
||||||
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
import { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style?: string
|
style?: string;
|
||||||
className?: string
|
className?: string;
|
||||||
text?: Store<ProseMirrorState>
|
text?: Store<ProseMirrorState>;
|
||||||
editorView?: Store<EditorView>
|
editorView?: Store<EditorView>;
|
||||||
extensions?: Store<ProseMirrorExtension[]>
|
extensions?: Store<ProseMirrorExtension[]>;
|
||||||
onInit: (s: EditorState, v: EditorView) => void
|
onInit: (s: EditorState, v: EditorView) => void;
|
||||||
onReconfigure: (s: EditorState) => void
|
onReconfigure: (s: EditorState) => void;
|
||||||
onChange: (s: EditorState) => void
|
onChange: (s: EditorState) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProseMirror = (props: Props) => {
|
export const ProseMirror = (props: Props) => {
|
||||||
|
@ -28,10 +28,9 @@ export const ProseMirror = (props: Props) => {
|
||||||
props.onChange(newState)
|
props.onChange(newState)
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(
|
createEffect((payload: [EditorState, ProseMirrorExtension[]]) => {
|
||||||
(payload: [EditorState, ProseMirrorExtension[]]) => {
|
|
||||||
const [prevText, prevExtensions] = payload
|
const [prevText, prevExtensions] = payload
|
||||||
const text = unwrap(props.text)
|
const text: EditorState = unwrap(props.text)
|
||||||
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
||||||
if (!text || !extensions?.length) {
|
if (!text || !extensions?.length) {
|
||||||
return [text, extensions]
|
return [text, extensions]
|
||||||
|
@ -60,7 +59,14 @@ export const ProseMirror = (props: Props) => {
|
||||||
[props.text, props.extensions]
|
[props.text, props.extensions]
|
||||||
)
|
)
|
||||||
|
|
||||||
return <div style={props.style} ref={editorRef} class={props.className} spell-check={false} />
|
return (
|
||||||
|
<div
|
||||||
|
style={props.style}
|
||||||
|
ref={editorRef}
|
||||||
|
className={props.className}
|
||||||
|
spell-check={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createEditorState = (
|
const createEditorState = (
|
||||||
|
@ -68,8 +74,8 @@ const createEditorState = (
|
||||||
extensions: ProseMirrorExtension[],
|
extensions: ProseMirrorExtension[],
|
||||||
prevText?: EditorState
|
prevText?: EditorState
|
||||||
): {
|
): {
|
||||||
editorState: EditorState
|
editorState: EditorState;
|
||||||
nodeViews: { [key: string]: NodeViewFn }
|
nodeViews: { [key: string]: NodeViewFn };
|
||||||
} => {
|
} => {
|
||||||
const reconfigure = text instanceof EditorState && prevText?.schema
|
const reconfigure = text instanceof EditorState && prevText?.schema
|
||||||
let schemaSpec = { nodes: {} }
|
let schemaSpec = { nodes: {} }
|
||||||
|
@ -95,7 +101,7 @@ const createEditorState = (
|
||||||
|
|
||||||
let editorState: EditorState
|
let editorState: EditorState
|
||||||
if (reconfigure) {
|
if (reconfigure) {
|
||||||
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
|
editorState = text.reconfigure({ schema, plugins })
|
||||||
} else if (text instanceof EditorState) {
|
} else if (text instanceof EditorState) {
|
||||||
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
||||||
} else if (text){
|
} else if (text){
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
|
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
||||||
import { unwrap } from 'solid-js/store'
|
import { unwrap } from 'solid-js/store'
|
||||||
import { undo, redo } from 'prosemirror-history'
|
import { undo, redo } from 'prosemirror-history'
|
||||||
import { Draft, useState } from '../store/context'
|
import { File, useState } from '../store/context'
|
||||||
|
import { mod } from '../env'
|
||||||
import * as remote from '../remote'
|
import * as remote from '../remote'
|
||||||
import { isEmpty /*, isInitialized*/ } from '../prosemirror/helpers'
|
import { isEmpty } from '../prosemirror/helpers'
|
||||||
import type { Styled } from './Layout'
|
import { Styled } from './Layout'
|
||||||
import '../styles/Sidebar.scss'
|
import '../styles/Sidebar.scss'
|
||||||
import { t } from '../../../utils/intl'
|
|
||||||
|
|
||||||
const Off = (props) => <div class="sidebar-off">{props.children}</div>
|
const Off = ({ children }: Styled) => <div class='sidebar-off'>{children}</div>
|
||||||
|
|
||||||
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
|
const Label = (props: Styled) => <h3 class='sidebar-label'>{props.children}</h3>
|
||||||
|
|
||||||
const Link = (
|
const Link = (
|
||||||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||||
) => (
|
) => (
|
||||||
<button
|
<button
|
||||||
class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
|
class={`sidebar-link${props.className ? ` ${props.className}` : ''}`}
|
||||||
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
|
style={{ marginBottom: props.withMargin ? '10px' : '' }}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
@ -27,59 +27,54 @@ const Link = (
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
||||||
const Keys = (props) => (
|
|
||||||
<span>
|
|
||||||
<For each={props.keys}>{(k: Element) => <i>{k}</i>}</For>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const [isMac, setIsMac] = createSignal(false)
|
|
||||||
onMount(() => setIsMac(window?.navigator.platform.includes('Mac')))
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
||||||
// const isDark = () => window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
const mod = isMac() ? 'Cmd' : 'Ctrl'
|
|
||||||
// const alt = isMac() ? 'Cmd' : 'Alt'
|
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
document.body.classList.toggle('dark')
|
document.body.classList.toggle('dark')
|
||||||
ctrl.updateConfig({ theme: document.body.className })
|
ctrl.updateConfig({ theme: document.body.className })
|
||||||
}
|
}
|
||||||
const collabText = () => {
|
const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
|
||||||
if (store.collab?.started) {
|
|
||||||
return t('Stop collab')
|
|
||||||
} else {
|
|
||||||
return store.collab?.error ? t('Restart collab') : t('Start collab')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const editorView = () => unwrap(store.editorView)
|
const editorView = () => unwrap(store.editorView)
|
||||||
const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
||||||
const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
|
const onOpenFile = (file: File) => ctrl.openFile(unwrap(file))
|
||||||
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
||||||
const onUndo = () => undo(editorView().state, editorView().dispatch)
|
const onUndo = () => undo(editorView().state, editorView().dispatch)
|
||||||
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
||||||
const onCopyAllAsMd = () =>
|
const onCopyAllAsMd = () => remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||||
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
|
||||||
const onDiscard = () => ctrl.discard()
|
const onDiscard = () => ctrl.discard()
|
||||||
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
||||||
const toggleSidebar = () => setIsHidden(!isHidden())
|
|
||||||
toggleSidebar()
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
const toggleSidebar = () => {
|
||||||
const DraftLink = (p: { draft: Draft }) => {
|
setIsHidden(!isHidden());
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar();
|
||||||
|
|
||||||
|
const onCollab = () => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileLink = (p: { file: File }) => {
|
||||||
const length = 100
|
const length = 100
|
||||||
let content = ''
|
let content = ''
|
||||||
const getContent = (node: any) => {
|
const getContent = (node: any) => {
|
||||||
if (node.text) content += node.text
|
if (node.text) {
|
||||||
|
content += node.text
|
||||||
|
}
|
||||||
|
|
||||||
if (content.length > length) {
|
if (content.length > length) {
|
||||||
content = content.slice(0, Math.max(0, length)) + '...'
|
content = content.substring(0, length) + '...'
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.content) {
|
if (node.content) {
|
||||||
for (const child of node.content) {
|
for (const child of node.content) {
|
||||||
if (content.length >= length) break
|
if (content.length >= length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
content = getContent(child)
|
content = getContent(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,90 +83,89 @@ export const Sidebar = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = () =>
|
const text = () =>
|
||||||
p.draft.path
|
p.file.path ? p.file.path.substring(p.file.path.length - length) : getContent(p.file.text?.doc)
|
||||||
? p.draft.path.slice(Math.max(0, p.draft.path.length - length))
|
|
||||||
: getContent(p.draft.text?.doc)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line solid/no-react-specific-props
|
<Link className='file' onClick={() => onOpenFile(p.file)} data-testid='open'>
|
||||||
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
|
{text()} {p.file.path && '📎'}
|
||||||
{text()} {p.draft.path && '📎'}
|
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCollab = () => {
|
const Keys = ({ keys }: { keys: string[] }) => (
|
||||||
const state = unwrap(store)
|
<span>
|
||||||
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
{keys.map((k) => (
|
||||||
}
|
<i>{k}</i>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (store.lastModified) setLastAction()
|
setLastAction(undefined)
|
||||||
})
|
}, store.lastModified)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (!lastAction()) return
|
if (!lastAction()) return
|
||||||
const id = setTimeout(() => {
|
const id = setTimeout(() => {
|
||||||
setLastAction()
|
setLastAction(undefined)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
onCleanup(() => clearTimeout(id))
|
onCleanup(() => clearTimeout(id))
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
<div className={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||||
<span class="sidebar-opener" onClick={toggleSidebar}>
|
<span className='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</span>
|
||||||
Советы и предложения
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Off onClick={() => editorView().focus()}>
|
<Off onClick={() => editorView().focus()}>
|
||||||
<div class="sidebar-closer" onClick={toggleSidebar} />
|
<div className='sidebar-closer' onClick={toggleSidebar}/>
|
||||||
<Show when={true}>
|
<Show when={true}>
|
||||||
<div>
|
<div>
|
||||||
{store.path && (
|
{store.path && (
|
||||||
<Label>
|
<Label>
|
||||||
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
<i>({store.path.substring(store.path.length - 24)})</i>
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
<Link>Пригласить соавторов</Link>
|
<Link>
|
||||||
<Link>Настройки публикации</Link>
|
Пригласить соавторов
|
||||||
<Link>История правок</Link>
|
</Link>
|
||||||
|
<Link>
|
||||||
|
Настройки публикации
|
||||||
|
</Link>
|
||||||
|
<Link>
|
||||||
|
История правок
|
||||||
|
</Link>
|
||||||
|
|
||||||
<div class="theme-switcher">
|
<div class='theme-switcher'>
|
||||||
Ночная тема
|
Ночная тема
|
||||||
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
<input type='checkbox' name='theme' id='theme' onClick={toggleTheme} />
|
||||||
<label for="theme">Ночная тема</label>
|
<label for='theme'>Ночная тема</label>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
onClick={onDiscard}
|
onClick={onDiscard}
|
||||||
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
disabled={!store.path && store.files.length === 0 && isEmpty(store.text)}
|
||||||
data-testid="discard"
|
data-testid='discard'
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line no-nested-ternary */}
|
{store.path ? 'Close' : store.files.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '}
|
||||||
{store.path
|
|
||||||
? 'Close'
|
|
||||||
: (store.drafts.length > 0 && isEmpty(store.text)
|
|
||||||
? 'Delete ⚠️'
|
|
||||||
: 'Clear')}{' '}
|
|
||||||
<Keys keys={[mod, 'w']} />
|
<Keys keys={[mod, 'w']} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onUndo}>
|
<Link onClick={onUndo}>
|
||||||
Undo <Keys keys={[mod, 'z']} />
|
Undo <Keys keys={[mod, 'z']} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onRedo}>
|
<Link onClick={onRedo}>
|
||||||
Redo <Keys keys={[mod, ...(isMac() ? ['Shift', 'z'] : ['y'])]} />
|
Redo <Keys keys={[mod, ...['Shift', 'z']]} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onToggleMarkdown} data-testid="markdown">
|
<Link onClick={onToggleMarkdown} data-testid='markdown'>
|
||||||
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
||||||
</Link>
|
</Link>
|
||||||
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
||||||
<Show when={store.drafts.length > 0}>
|
<Show when={store.files.length > 0}>
|
||||||
<h4>Drafts:</h4>
|
<h4>Drafts:</h4>
|
||||||
<p>
|
<p>
|
||||||
<For each={store.drafts}>{(draft: Draft) => <DraftLink draft={draft} />}</For>
|
<For each={store.files}>{(file) => <FileLink file={file} />}</For>
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
||||||
{collabText()}
|
Collab {collabText()}
|
||||||
</Link>
|
</Link>
|
||||||
<Show when={collabUsers() > 0}>
|
<Show when={collabUsers() > 0}>
|
||||||
<span>
|
<span>
|
||||||
|
|
3
src/components/Editor/env.ts
Normal file
3
src/components/Editor/env.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const isDark = () => (window as any).matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
export const mod = 'Ctrl'
|
||||||
|
export const alt = 'Alt'
|
|
@ -1,7 +1,7 @@
|
||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
import { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
import { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
||||||
import type { Node, Schema } from 'prosemirror-model'
|
import { Node, Schema } from 'prosemirror-model'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
|
|
||||||
export const serialize = (state: EditorState) => {
|
export const serialize = (state: EditorState) => {
|
||||||
let text = markdownSerializer.serialize(state.doc)
|
let text = markdownSerializer.serialize(state.doc)
|
||||||
|
@ -12,24 +12,10 @@ export const serialize = (state: EditorState) => {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
const findAlignment = (cell: Node): string | null => {
|
|
||||||
const alignment = cell.attrs.style as string
|
|
||||||
if (!alignment) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = alignment.match(/text-align: ?(left|right|center)/)
|
|
||||||
if (match && match[1]) {
|
|
||||||
return match[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const markdownSerializer = new MarkdownSerializer(
|
export const markdownSerializer = new MarkdownSerializer(
|
||||||
{
|
{
|
||||||
...defaultMarkdownSerializer.nodes,
|
...defaultMarkdownSerializer.nodes,
|
||||||
image(state: any, node) {
|
image(state, node) {
|
||||||
const alt = state.esc(node.attrs.alt || '')
|
const alt = state.esc(node.attrs.alt || '')
|
||||||
const src = node.attrs.path ?? node.attrs.src
|
const src = node.attrs.path ?? node.attrs.src
|
||||||
const title = node.attrs.title ? state.quote(node.attrs.title) : undefined
|
const title = node.attrs.title ? state.quote(node.attrs.title) : undefined
|
||||||
|
@ -102,6 +88,20 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
return findAlignment(cell)
|
return findAlignment(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findAlignment(cell: Node): string | null {
|
||||||
|
const alignment = cell.attrs.style as string
|
||||||
|
if (!alignment) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = alignment.match(/text-align:[ ]?(left|right|center)/)
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
node.forEach((table_child) => {
|
node.forEach((table_child) => {
|
||||||
if (table_child.type.name === 'table_head') serializeTableHead(table_child)
|
if (table_child.type.name === 'table_head') serializeTableHead(table_child)
|
||||||
if (table_child.type.name === 'table_body') serializeTableBody(table_child)
|
if (table_child.type.name === 'table_body') serializeTableBody(table_child)
|
||||||
|
@ -122,10 +122,9 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function listIsTight(tokens: any[], idx: number) {
|
function listIsTight(tokens: any, i: number) {
|
||||||
let i = idx
|
|
||||||
while (++i < tokens.length) {
|
while (++i < tokens.length) {
|
||||||
if (tokens[i].type !== 'list_item_open') return tokens[i].hidden
|
if (tokens[i].type != 'list_item_open') return tokens[i].hidden
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { schema as markdownSchema } from 'prosemirror-markdown'
|
import { schema as markdownSchema } from 'prosemirror-markdown'
|
||||||
import type OrderedMap from 'orderedmap'
|
import { Schema } from 'prosemirror-model'
|
||||||
import { NodeSpec, Schema } from 'prosemirror-model'
|
|
||||||
import { baseKeymap } from 'prosemirror-commands'
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
import { sinkListItem, liftListItem } from 'prosemirror-schema-list'
|
import { sinkListItem, liftListItem } from 'prosemirror-schema-list'
|
||||||
import { history } from 'prosemirror-history'
|
import { history } from 'prosemirror-history'
|
||||||
import { dropCursor } from 'prosemirror-dropcursor'
|
import { dropCursor } from 'prosemirror-dropcursor'
|
||||||
import { buildKeymap } from 'prosemirror-example-setup'
|
import { buildKeymap } from 'prosemirror-example-setup'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const plainSchema = new Schema({
|
const plainSchema = new Schema({
|
||||||
nodes: {
|
nodes: {
|
||||||
|
@ -40,10 +39,7 @@ export default (plain = false): ProseMirrorExtension => ({
|
||||||
marks: plainSchema.spec.marks
|
marks: plainSchema.spec.marks
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update(
|
nodes: (markdownSchema.spec.nodes as any).update('blockquote', blockquoteSchema),
|
||||||
'blockquote',
|
|
||||||
blockquoteSchema as unknown as NodeSpec
|
|
||||||
),
|
|
||||||
marks: markdownSchema.spec.marks
|
marks: markdownSchema.spec.marks
|
||||||
},
|
},
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { inputRules } from 'prosemirror-inputrules'
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
import type { Mark, MarkType } from 'prosemirror-model'
|
import { Mark, MarkType } from 'prosemirror-model'
|
||||||
import type { EditorState, Transaction } from 'prosemirror-state'
|
import { EditorState, Transaction } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import { markInputRule } from './mark-input-rule'
|
import { markInputRule } from './mark-input-rule'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const blank = '\u00A0'
|
const blank = '\xa0'
|
||||||
|
|
||||||
const onArrow =
|
const onArrow =
|
||||||
(dir: 'left' | 'right') =>
|
(dir: 'left' | 'right') =>
|
||||||
|
@ -36,7 +36,7 @@ const codeKeymap = {
|
||||||
ArrowRight: onArrow('right')
|
ArrowRight: onArrow('right')
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeRule = (nodeType: MarkType) => markInputRule(/`([^`]+)`$/, nodeType, null)
|
const codeRule = (nodeType: MarkType) => markInputRule(/(?:`)([^`]+)(?:`)$/, nodeType)
|
||||||
|
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
||||||
import type { YOptions } from '../../store/context'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { YOptions } from '../../store/context'
|
||||||
|
|
||||||
export interface EditingProps {
|
export const cursorBuilder = (user: any): HTMLElement => {
|
||||||
name: string
|
|
||||||
foreground: string
|
|
||||||
background: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const cursorBuilder = (user: EditingProps): HTMLElement => {
|
|
||||||
const cursor = document.createElement('span')
|
const cursor = document.createElement('span')
|
||||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||||
cursor.setAttribute('style', `border-color: ${user.background}`)
|
cursor.setAttribute('style', `border-color: ${user.background}`)
|
||||||
|
@ -25,6 +19,7 @@ export default (y: YOptions): ProseMirrorExtension => ({
|
||||||
? [
|
? [
|
||||||
...prev,
|
...prev,
|
||||||
ySyncPlugin(y.type),
|
ySyncPlugin(y.type),
|
||||||
|
// @ts-ignore
|
||||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||||
yUndoPlugin()
|
yUndoPlugin()
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { Plugin, NodeSelection } from 'prosemirror-state'
|
import { Plugin, NodeSelection } from 'prosemirror-state'
|
||||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import handleIcon from '../../../../assets/handle.svg'
|
|
||||||
|
const handleIcon = `
|
||||||
|
<svg viewBox="0 0 10 10" height="14" width="14">
|
||||||
|
<path d="M3 2a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm4-8a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2z"/>
|
||||||
|
</svg>`
|
||||||
|
|
||||||
const createDragHandle = () => {
|
const createDragHandle = () => {
|
||||||
const handle = document.createElement('span')
|
const handle = document.createElement('span')
|
||||||
|
@ -18,8 +22,8 @@ const handlePlugin = new Plugin({
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
const decos = []
|
const decos = []
|
||||||
state.doc.forEach((node, pos) => {
|
state.doc.forEach((node, pos) => {
|
||||||
|
decos.push(Decoration.widget(pos + 1, createDragHandle))
|
||||||
decos.push(
|
decos.push(
|
||||||
Decoration.widget(pos + 1, createDragHandle),
|
|
||||||
Decoration.node(pos, pos + node.nodeSize, {
|
Decoration.node(pos, pos + node.nodeSize, {
|
||||||
class: 'draggable'
|
class: 'draggable'
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { Node, NodeSpec, Schema } from 'prosemirror-model'
|
import { Node, Schema } from 'prosemirror-model'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import type OrderedMap from 'orderedmap'
|
|
||||||
|
|
||||||
const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
|
const REGEX = /^!\[([^[\]]*?)\]\((.+?)\)\s+/
|
||||||
const MAX_MATCH = 500
|
const MAX_MATCH = 500
|
||||||
|
|
||||||
const isUrl = (str: string) => {
|
const isUrl = (str: string) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(str)
|
const url = new URL(str)
|
||||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||||
} catch {
|
} catch (_) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
const isBlank = (text: string) => text === ' ' || text === '\xa0'
|
||||||
|
|
||||||
const imageInput = (schema: Schema, _path?: string) =>
|
const imageInput = (schema: Schema, path?: string) =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
props: {
|
props: {
|
||||||
handleTextInput(view, from, to, text) {
|
handleTextInput(view, from, to, text) {
|
||||||
|
@ -30,7 +29,7 @@ const imageInput = (schema: Schema, _path?: string) =>
|
||||||
Math.max(0, $from.parentOffset - MAX_MATCH),
|
Math.max(0, $from.parentOffset - MAX_MATCH),
|
||||||
$from.parentOffset,
|
$from.parentOffset,
|
||||||
null,
|
null,
|
||||||
'\uFFFC'
|
'\ufffc'
|
||||||
) + text
|
) + text
|
||||||
|
|
||||||
const match = REGEX.exec(textBefore)
|
const match = REGEX.exec(textBefore)
|
||||||
|
@ -45,6 +44,7 @@ const imageInput = (schema: Schema, _path?: string) =>
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ const imageSchema = {
|
||||||
src: dom.getAttribute('src'),
|
src: dom.getAttribute('src'),
|
||||||
title: dom.getAttribute('title'),
|
title: dom.getAttribute('title'),
|
||||||
alt: dom.getAttribute('alt'),
|
alt: dom.getAttribute('alt'),
|
||||||
path: (dom as NodeSpec).dataset.path
|
path: dom.getAttribute('data-path')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -102,12 +102,12 @@ class ImageView {
|
||||||
contentDOM: Element
|
contentDOM: Element
|
||||||
container: HTMLElement
|
container: HTMLElement
|
||||||
handle: HTMLElement
|
handle: HTMLElement
|
||||||
onResizeFn: (e: Event) => void
|
onResizeFn: any
|
||||||
onResizeEndFn: (e: Event) => void
|
onResizeEndFn: any
|
||||||
width: number
|
width: number
|
||||||
updating: 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.node = node
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos
|
this.getPos = getPos
|
||||||
|
@ -162,12 +162,12 @@ class ImageView {
|
||||||
export default (path?: string): ProseMirrorExtension => ({
|
export default (path?: string): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
nodes: (prev.nodes as OrderedMap<NodeSpec>).update('image', imageSchema as unknown as NodeSpec)
|
nodes: (prev.nodes as any).update('image', imageSchema)
|
||||||
}),
|
}),
|
||||||
plugins: (prev, schema) => [...prev, imageInput(schema, path)],
|
plugins: (prev, schema) => [...prev, imageInput(schema, path)],
|
||||||
nodeViews: {
|
nodeViews: {
|
||||||
image: (node, view, getPos) => {
|
image: (node, view, getPos) => {
|
||||||
return new ImageView(node, view, getPos, view.state.schema, path)
|
return new ImageView(node, view, getPos, view.state.schema, path)
|
||||||
}
|
}
|
||||||
} as unknown as { [key: string]: NodeViewFn }
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import type { Mark, Node, Schema } from 'prosemirror-model'
|
import { Mark, Node, Schema } from 'prosemirror-model'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
const REGEX = /(^|\s)\[(.+)\]\(([^ ]+)(?: "(.+)")?\)/
|
||||||
|
|
||||||
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
||||||
let markPos = { from: -1, to: -1 }
|
let markPos = { from: -1, to: -1 }
|
||||||
|
@ -29,8 +29,9 @@ const markdownLinks = (schema: Schema) =>
|
||||||
apply(tr, state) {
|
apply(tr, state) {
|
||||||
const action = tr.getMeta(this)
|
const action = tr.getMeta(this)
|
||||||
if (action?.pos) {
|
if (action?.pos) {
|
||||||
(state as any).pos = action.pos
|
state.pos = action.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -53,12 +54,11 @@ const markdownLinks = (schema: Schema) =>
|
||||||
const resolvePos = (view: EditorView, pos: number) => {
|
const resolvePos = (view: EditorView, pos: number) => {
|
||||||
try {
|
try {
|
||||||
return view.state.doc.resolve(pos)
|
return view.state.doc.resolve(pos)
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const toLink = (view: EditorView, tr: Transaction) => {
|
const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
const sel = view.state.selection
|
const sel = view.state.selection
|
||||||
const state = pluginKey.getState(view.state)
|
const state = pluginKey.getState(view.state)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule } from 'prosemirror-inputrules'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import type { MarkType } from 'prosemirror-model'
|
import { MarkType } from 'prosemirror-model'
|
||||||
|
|
||||||
export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs) =>
|
export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = undefined) =>
|
||||||
new InputRule(regexp, (state: EditorState, match: string[], start: number, end: number) => {
|
new InputRule(regexp, (state: EditorState, match: string[], start: number, end: number) => {
|
||||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
|
@ -13,6 +13,7 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs) =>
|
||||||
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
||||||
if (node.marks.length > 0) {
|
if (node.marks.length > 0) {
|
||||||
hasMarks = true
|
hasMarks = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -22,7 +23,6 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs) =>
|
||||||
|
|
||||||
if (textEnd < end) tr.delete(textEnd, end)
|
if (textEnd < end) tr.delete(textEnd, end)
|
||||||
if (textStart > start) tr.delete(start, textStart)
|
if (textStart > start) tr.delete(start, textStart)
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
end = start + match[1].length
|
end = start + match[1].length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import {
|
||||||
emDash,
|
emDash,
|
||||||
ellipsis
|
ellipsis
|
||||||
} from 'prosemirror-inputrules'
|
} from 'prosemirror-inputrules'
|
||||||
import type { NodeType, Schema } from 'prosemirror-model'
|
import { NodeType, Schema } from 'prosemirror-model'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
||||||
|
|
||||||
|
@ -16,10 +16,10 @@ const orderedListRule = (nodeType: NodeType) =>
|
||||||
/^(\d+)\.\s$/,
|
/^(\d+)\.\s$/,
|
||||||
nodeType,
|
nodeType,
|
||||||
(match) => ({ order: +match[1] }),
|
(match) => ({ order: +match[1] }),
|
||||||
(match, node) => node.childCount + node.attrs.order === +match[1]
|
(match, node) => node.childCount + node.attrs.order == +match[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([*+-])\s$/, nodeType)
|
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([-+*])\s$/, nodeType)
|
||||||
|
|
||||||
const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
||||||
textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({
|
textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({
|
||||||
|
@ -27,7 +27,7 @@ const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const markdownRules = (schema: Schema) => {
|
const markdownRules = (schema: Schema) => {
|
||||||
const rules = [...smartQuotes, ellipsis, emDash]
|
const rules = smartQuotes.concat(ellipsis, emDash)
|
||||||
if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote))
|
if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote))
|
||||||
if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list))
|
if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list))
|
||||||
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list))
|
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list))
|
||||||
|
|
|
@ -13,40 +13,38 @@ import {
|
||||||
Dropdown
|
Dropdown
|
||||||
} from 'prosemirror-menu'
|
} from 'prosemirror-menu'
|
||||||
|
|
||||||
import type { MenuItemSpec, MenuElement } from 'prosemirror-menu'
|
|
||||||
|
|
||||||
import { wrapInList } from 'prosemirror-schema-list'
|
import { wrapInList } from 'prosemirror-schema-list'
|
||||||
import { Command, EditorState, NodeSelection, Transaction } from 'prosemirror-state'
|
import { NodeSelection } from 'prosemirror-state'
|
||||||
|
|
||||||
import { TextField, openPrompt } from './prompt'
|
import { TextField, openPrompt } from './prompt'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import type { Attrs, MarkType, NodeType, Schema } from 'prosemirror-model'
|
|
||||||
import type { EditorView } from 'prosemirror-view'
|
|
||||||
|
|
||||||
// Helpers to create specific types of items
|
// Helpers to create specific types of items
|
||||||
|
|
||||||
function canInsert(state: EditorState, nodeType: NodeType) {
|
function canInsert(state, nodeType) {
|
||||||
const $from = state.selection.$from
|
const $from = state.selection.$from
|
||||||
|
|
||||||
for (let d = $from.depth; d >= 0; d--) {
|
for (let d = $from.depth; d >= 0; d--) {
|
||||||
const index = $from.index(d)
|
const index = $from.index(d)
|
||||||
|
|
||||||
if ($from.node(d).canReplaceWith(index, index, nodeType)) return true
|
if ($from.node(d).canReplaceWith(index, index, nodeType)) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertImageItem(nodeType: NodeType) {
|
function insertImageItem(nodeType) {
|
||||||
return new MenuItem({
|
return new MenuItem({
|
||||||
icon: icons.image,
|
icon: icons.image,
|
||||||
label: 'image',
|
label: 'image',
|
||||||
enable(state) {
|
enable(state) {
|
||||||
return canInsert(state, nodeType)
|
return canInsert(state, nodeType)
|
||||||
},
|
},
|
||||||
run(state: EditorState, _, view: EditorView) {
|
run(state, _, view) {
|
||||||
const { from, to, node } = state.selection as NodeSelection
|
const { from, to } = state.selection
|
||||||
let attrs = null
|
let attrs = null
|
||||||
if (state.selection instanceof NodeSelection && node.type === nodeType) {
|
|
||||||
attrs = node.attrs
|
if (state.selection instanceof NodeSelection && state.selection.node.type == nodeType) { attrs = state.selection.node.attrs }
|
||||||
}
|
|
||||||
|
|
||||||
openPrompt({
|
openPrompt({
|
||||||
title: 'Insert image',
|
title: 'Insert image',
|
||||||
|
@ -62,8 +60,7 @@ function insertImageItem(nodeType: NodeType) {
|
||||||
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line no-shadow
|
callback(attrs) {
|
||||||
callback(attrs: Attrs) {
|
|
||||||
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))
|
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))
|
||||||
view.focus()
|
view.focus()
|
||||||
}
|
}
|
||||||
|
@ -72,31 +69,41 @@ function insertImageItem(nodeType: NodeType) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmdItem(cmd: Command, options: MenuItemSpec) {
|
function cmdItem(cmd, options) {
|
||||||
const passedOptions = { label: options.title, run: cmd } as MenuItemSpec
|
const passedOptions = {
|
||||||
Object.keys(options).forEach((prop) => (passedOptions[prop] = options[prop]))
|
label: options.title,
|
||||||
// TODO: enable/disable items logix
|
run: cmd
|
||||||
passedOptions.select = (state) => cmd(state)
|
|
||||||
return new MenuItem(passedOptions as MenuItemSpec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function markActive(state: EditorState, type: MarkType) {
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
|
if ((!options.enable || options.enable === true) && !options.select) { passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state) }
|
||||||
|
|
||||||
|
return new MenuItem(passedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function markActive(state, type) {
|
||||||
const { from, $from, to, empty } = state.selection
|
const { from, $from, to, empty } = state.selection
|
||||||
|
|
||||||
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
||||||
|
|
||||||
return state.doc.rangeHasMark(from, to, type)
|
return state.doc.rangeHasMark(from, to, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
function markItem(markType: MarkType, options: MenuItemSpec) {
|
function markItem(markType, options) {
|
||||||
const passedOptions = {
|
const passedOptions = {
|
||||||
active(state) {
|
active(state) {
|
||||||
return markActive(state, markType)
|
return markActive(state, markType)
|
||||||
|
},
|
||||||
|
enable: true
|
||||||
}
|
}
|
||||||
} as MenuItemSpec
|
|
||||||
Object.keys(options).forEach((prop: string) => (passedOptions[prop] = options[prop]))
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
return cmdItem(toggleMark(markType), passedOptions)
|
return cmdItem(toggleMark(markType), passedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkItem(markType: MarkType) {
|
function linkItem(markType) {
|
||||||
return new MenuItem({
|
return new MenuItem({
|
||||||
title: 'Add or remove link',
|
title: 'Add or remove link',
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -104,21 +111,27 @@ function linkItem(markType: MarkType) {
|
||||||
height: 18,
|
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'
|
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) => Boolean(markActive(state, markType)),
|
active(state) {
|
||||||
enable: (state: EditorState) => !state.selection.empty,
|
return markActive(state, markType)
|
||||||
run(state: EditorState, dispatch: (t: Transaction) => void, view: EditorView) {
|
},
|
||||||
|
enable(state) {
|
||||||
|
return !state.selection.empty
|
||||||
|
},
|
||||||
|
run(state, dispatch, view) {
|
||||||
if (markActive(state, markType)) {
|
if (markActive(state, markType)) {
|
||||||
toggleMark(markType)(state, dispatch)
|
toggleMark(markType)(state, dispatch)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
openPrompt({
|
openPrompt({
|
||||||
fields: {
|
fields: {
|
||||||
href: new TextField({
|
href: new TextField({
|
||||||
label: 'Link target',
|
label: 'Link target',
|
||||||
required: true
|
required: true
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
callback(attrs: Attrs) {
|
callback(attrs) {
|
||||||
toggleMark(markType, attrs)(view.state, view.dispatch)
|
toggleMark(markType, attrs)(view.state, view.dispatch)
|
||||||
view.focus()
|
view.focus()
|
||||||
}
|
}
|
||||||
|
@ -127,8 +140,7 @@ function linkItem(markType: MarkType) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapListItem(nodeType: NodeType, options: MenuItemSpec & { attrs: Attrs }) {
|
function wrapListItem(nodeType, options) {
|
||||||
options.run = (_) => true
|
|
||||||
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,24 +202,9 @@ function wrapListItem(nodeType: NodeType, options: MenuItemSpec & { attrs: Attrs
|
||||||
// **`fullMenu`**`: [[MenuElement]]`
|
// **`fullMenu`**`: [[MenuElement]]`
|
||||||
// : An array of arrays of menu elements for use as the full menu
|
// : An array of arrays of menu elements for use as the full menu
|
||||||
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
|
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
|
||||||
/*
|
export function buildMenuItems(schema) {
|
||||||
type BuildSchema = {
|
|
||||||
marks: { strong: any; em: any; code: any; link: any; blockquote: any }
|
|
||||||
nodes: {
|
|
||||||
image: any
|
|
||||||
bullet_list: any
|
|
||||||
ordered_list: any
|
|
||||||
blockquote: any
|
|
||||||
paragraph: any
|
|
||||||
code_block: any
|
|
||||||
heading: any
|
|
||||||
horizontal_rule: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
export function buildMenuItems(schema: Schema) {
|
|
||||||
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
||||||
let type: NodeType | MarkType
|
let type
|
||||||
|
|
||||||
if ((type = schema.marks.strong)) {
|
if ((type = schema.marks.strong)) {
|
||||||
r.toggleStrong = markItem(type, {
|
r.toggleStrong = markItem(type, {
|
||||||
|
@ -215,9 +212,9 @@ export function buildMenuItems(schema: Schema) {
|
||||||
icon: {
|
icon: {
|
||||||
width: 13,
|
width: 13,
|
||||||
height: 16,
|
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'
|
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)) {
|
if ((type = schema.marks.em)) {
|
||||||
|
@ -226,21 +223,21 @@ export function buildMenuItems(schema: Schema) {
|
||||||
icon: {
|
icon: {
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 16,
|
height: 16,
|
||||||
path: 'M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z'
|
path: "M4.39216 0V3.42857H6.81882L3.06353 12.5714H0V16H8.78431V12.5714H6.35765L10.1129 3.42857H13.1765V0H4.39216Z"
|
||||||
}
|
}
|
||||||
} as MenuItemSpec)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type = schema.marks.code)) {
|
if ((type = schema.marks.code)) {
|
||||||
r.toggleCode = markItem(type, {
|
r.toggleCode = markItem(type, {
|
||||||
title: 'Toggle code font',
|
title: 'Toggle code font',
|
||||||
icon: icons.code
|
icon: icons.code
|
||||||
} as MenuItemSpec)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type = schema.marks.link)) r.toggleLink = linkItem(type)
|
if ((type = schema.marks.link)) r.toggleLink = linkItem(type)
|
||||||
|
|
||||||
if ((type = schema.marks.blockquote) && (type = schema.nodes.image)) r.insertImage = insertImageItem(type)
|
if ((type = schema.marks.blockquote)) { if ((type = schema.nodes.image)) r.insertImage = insertImageItem(type) }
|
||||||
|
|
||||||
if ((type = schema.nodes.bullet_list)) {
|
if ((type = schema.nodes.bullet_list)) {
|
||||||
r.wrapBulletList = wrapListItem(type, {
|
r.wrapBulletList = wrapListItem(type, {
|
||||||
|
@ -250,7 +247,7 @@ export function buildMenuItems(schema: Schema) {
|
||||||
height: 16,
|
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'
|
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)) {
|
if ((type = schema.nodes.ordered_list)) {
|
||||||
|
@ -261,7 +258,7 @@ export function buildMenuItems(schema: Schema) {
|
||||||
height: 16,
|
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'
|
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)) {
|
if ((type = schema.nodes.blockquote)) {
|
||||||
|
@ -312,36 +309,28 @@ export function buildMenuItems(schema: Schema) {
|
||||||
r.insertHorizontalRule = new MenuItem({
|
r.insertHorizontalRule = new MenuItem({
|
||||||
label: '---',
|
label: '---',
|
||||||
icon: icons.horizontal_rule,
|
icon: icons.horizontal_rule,
|
||||||
enable: (state) => canInsert(state, hr),
|
enable(state) {
|
||||||
run(state: EditorState, dispatch: (tr: Transaction) => void) {
|
return canInsert(state, hr)
|
||||||
|
},
|
||||||
|
run(state, dispatch) {
|
||||||
dispatch(state.tr.replaceSelectionWith(hr.create()))
|
dispatch(state.tr.replaceSelectionWith(hr.create()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tMenu = new Dropdown(
|
const cut = (arr) => arr.filter((x) => x)
|
||||||
[
|
r.typeMenu = new Dropdown(
|
||||||
r.makeHead1 as MenuElement,
|
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
|
||||||
r.makeHead2 as MenuElement,
|
{ label: 'Тт', icon: {
|
||||||
r.makeHead3 as MenuElement,
|
|
||||||
r.typeMenu as MenuElement,
|
|
||||||
r.wrapBlockQuote as MenuElement
|
|
||||||
],
|
|
||||||
{
|
|
||||||
label: 'Тт',
|
|
||||||
// FIXME !!!!!!!!!
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: {
|
|
||||||
width: 12,
|
width: 12,
|
||||||
height: 12,
|
height: 12,
|
||||||
path: 'M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z'
|
path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
||||||
}
|
} })
|
||||||
}
|
// r.blockMenu = []
|
||||||
)
|
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
|
||||||
r.listMenu = [r.wrapBulletList as MenuItem, r.wrapOrderedList as MenuItem]
|
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
||||||
r.inlineMenu = [r.toggleStrong as MenuItem, r.toggleEm as MenuItem, r.toggleMark as MenuItem]
|
r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu)
|
||||||
r.fullMenu = [...r.inlineMenu, tMenu as MenuItem, ...r.listMenu].filter(Boolean)
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,8 +338,8 @@ export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
...prev,
|
...prev,
|
||||||
menuBar({
|
menuBar({
|
||||||
floating: true,
|
floating: false,
|
||||||
content: buildMenuItems(schema).fullMenu as any // NOTE: MenuItem and MenuElement are compatible
|
content: buildMenuItems(schema).fullMenu
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
|
import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import { createMarkdownParser } from '../../markdown'
|
import { createMarkdownParser } from '../../markdown'
|
||||||
|
|
||||||
const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:\d+)?(\/|\/([\w!#%&+./:=?@-]))?/g
|
const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/g
|
||||||
|
|
||||||
const transform = (schema: Schema, fragment: Fragment) => {
|
const transform = (schema: Schema, fragment: Fragment) => {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
fragment.forEach((child: Node) => {
|
fragment.forEach((child: Node) => {
|
||||||
if (child.isText) {
|
if (child.isText) {
|
||||||
let pos = 0
|
let pos = 0
|
||||||
let match
|
let match: any
|
||||||
|
|
||||||
while ((match = URL_REGEX.exec(child.text)) !== null) {
|
while ((match = URL_REGEX.exec(child.text)) !== null) {
|
||||||
const start = match.index
|
const start = match.index
|
||||||
|
@ -64,7 +64,7 @@ const pasteMarkdown = (schema: Schema) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const paste = parser.parse(text)
|
const paste = parser.parse(text)
|
||||||
const slice = paste as Node & { openStart: number; openEnd: number }
|
const slice = paste.slice(0)
|
||||||
const fragment = shiftKey ? slice.content : transform(schema, slice.content)
|
const fragment = shiftKey ? slice.content : transform(schema, slice.content)
|
||||||
const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd))
|
const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
import { isEmpty, ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension, isEmpty } from '../helpers'
|
||||||
|
|
||||||
const placeholder = (text: string) =>
|
const placeholder = (text: string) =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
const prefix = 'ProseMirror-prompt'
|
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'))
|
const wrapper = document.body.appendChild(document.createElement('div'))
|
||||||
wrapper.className = prefix
|
wrapper.className = prefix
|
||||||
const mouseOutside = (e: MouseEvent) => {
|
|
||||||
if (!wrapper.contains(e.target as Node)) close()
|
const mouseOutside = (e: any) => {
|
||||||
|
if (!wrapper.contains(e.target)) close()
|
||||||
}
|
}
|
||||||
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
||||||
const close = () => {
|
const close = () => {
|
||||||
window.removeEventListener('mousedown', mouseOutside)
|
window.removeEventListener('mousedown', mouseOutside)
|
||||||
if (wrapper.parentNode) wrapper.remove()
|
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
const domFields = []
|
const domFields: any = []
|
||||||
options.fields.forEach((name) => {
|
options.fields.forEach((name) => {
|
||||||
domFields.push(options.fields[name].render())
|
domFields.push(options.fields[name].render())
|
||||||
})
|
})
|
||||||
|
@ -32,7 +32,7 @@ export function openPrompt(options) {
|
||||||
if (options.title) {
|
if (options.title) {
|
||||||
form.appendChild(document.createElement('h5')).textContent = options.title
|
form.appendChild(document.createElement('h5')).textContent = options.title
|
||||||
}
|
}
|
||||||
domFields.forEach((field) => {
|
domFields.forEach((field: any) => {
|
||||||
form.appendChild(document.createElement('div')).appendChild(field)
|
form.appendChild(document.createElement('div')).appendChild(field)
|
||||||
})
|
})
|
||||||
const buttons = form.appendChild(document.createElement('div'))
|
const buttons = form.appendChild(document.createElement('div'))
|
||||||
|
@ -59,25 +59,24 @@ export function openPrompt(options) {
|
||||||
})
|
})
|
||||||
|
|
||||||
form.addEventListener('keydown', (e) => {
|
form.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.keyCode == 27) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
close()
|
close()
|
||||||
// eslint-disable-next-line unicorn/prefer-keyboard-event-key
|
} else if (e.keyCode == 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
|
||||||
} else if (e.keyCode === 13 && !(e.ctrlKey || e.metaKey || e.shiftKey)) {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
submit()
|
submit()
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.keyCode == 9) {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (!wrapper.contains(document.activeElement)) close()
|
if (!wrapper.contains(document.activeElement)) close()
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const inpel = form.elements[0] as HTMLInputElement
|
const input: any = form.elements[0]
|
||||||
if (inpel) inpel.focus()
|
if (input) input.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValues(fields, domFields) {
|
function getValues(fields: any, domFields: any) {
|
||||||
const result = Object.create(null)
|
const result = Object.create(null)
|
||||||
let i = 0
|
let i = 0
|
||||||
fields.forEarch((name) => {
|
fields.forEarch((name) => {
|
||||||
|
@ -94,35 +93,23 @@ function getValues(fields, domFields) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportInvalid(dom: HTMLElement, message: string) {
|
function reportInvalid(dom: any, message: any) {
|
||||||
const parent = dom.parentNode
|
const parent = dom.parentNode
|
||||||
const msg = parent.appendChild(document.createElement('div'))
|
const msg = parent.appendChild(document.createElement('div'))
|
||||||
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
||||||
msg.style.top = dom.offsetTop - 5 + 'px'
|
msg.style.top = dom.offsetTop - 5 + 'px'
|
||||||
msg.className = 'ProseMirror-invalid'
|
msg.className = 'ProseMirror-invalid'
|
||||||
msg.textContent = message
|
msg.textContent = message
|
||||||
// eslint-disable-next-line unicorn/prefer-dom-node-remove
|
|
||||||
setTimeout(() => parent.removeChild(msg), 1500)
|
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 {
|
export class Field {
|
||||||
options: FieldOptions
|
options: any
|
||||||
constructor(options) {
|
constructor(options: any) {
|
||||||
this.options = options
|
this.options = options
|
||||||
}
|
}
|
||||||
|
|
||||||
read(dom) {
|
read(dom: any) {
|
||||||
return dom.value
|
return dom.value
|
||||||
}
|
}
|
||||||
// :: (any) → ?string
|
// :: (any) → ?string
|
||||||
|
@ -131,12 +118,13 @@ export class Field {
|
||||||
return typeof _value === typeof ''
|
return typeof _value === typeof ''
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(value) {
|
validate(value: any) {
|
||||||
if (!value && this.options.required) return 'Required field'
|
if (!value && this.options.required) return 'Required field'
|
||||||
|
|
||||||
return this.validateType(value) || (this.options.validate && this.options.validate(value))
|
return this.validateType(value) || (this.options.validate && this.options.validate(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
clean(value) {
|
clean(value: any) {
|
||||||
return this.options.clean ? this.options.clean(value) : value
|
return this.options.clean ? this.options.clean(value) : value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,10 +144,10 @@ export class TextField extends Field {
|
||||||
export class SelectField extends Field {
|
export class SelectField extends Field {
|
||||||
render() {
|
render() {
|
||||||
const select = document.createElement('select')
|
const select = document.createElement('select')
|
||||||
this.options.options.forEach((o) => {
|
this.options.options.forEach((o: { value: string; label: string }) => {
|
||||||
const opt = select.appendChild(document.createElement('option'))
|
const opt = select.appendChild(document.createElement('option'))
|
||||||
opt.value = o.value
|
opt.value = o.value
|
||||||
opt.selected = o.value === this.options.value
|
opt.selected = o.value == this.options.value
|
||||||
opt.label = o.label
|
opt.label = o.label
|
||||||
})
|
})
|
||||||
return select
|
return select
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const scroll = (view: EditorView) => {
|
const scroll = (view: EditorView) => {
|
||||||
if (!view.state.selection.empty) return false
|
if (!view.state.selection.empty) return false
|
||||||
|
|
|
@ -1,57 +1,60 @@
|
||||||
import { renderGrouped } from 'prosemirror-menu'
|
import { renderGrouped } from "prosemirror-menu";
|
||||||
import { EditorState, Plugin } from 'prosemirror-state'
|
import { Plugin } from "prosemirror-state";
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { ProseMirrorExtension } from "../helpers";
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { buildMenuItems } from "./menu";
|
||||||
import { buildMenuItems } from './menu'
|
|
||||||
|
|
||||||
export class SelectionTooltip {
|
export class SelectionTooltip {
|
||||||
tooltip: HTMLElement
|
tooltip: any;
|
||||||
|
|
||||||
constructor(view: EditorView, schema) {
|
constructor(view: any, schema: any) {
|
||||||
this.tooltip = document.createElement('div')
|
this.tooltip = document.createElement("div");
|
||||||
this.tooltip.className = 'tooltip'
|
this.tooltip.className = "tooltip";
|
||||||
view.dom.parentNode.appendChild(this.tooltip)
|
view.dom.parentNode.appendChild(this.tooltip);
|
||||||
console.debug('[prosemirror] selection view', view)
|
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu);
|
||||||
console.debug('[prosemirror] selection menu', buildMenuItems(schema).fullMenu)
|
this.tooltip.appendChild(dom);
|
||||||
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
this.update(view, null);
|
||||||
this.tooltip.appendChild(dom)
|
|
||||||
this.update(view, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(view: EditorView, lastState: EditorState) {
|
update(view: any, lastState: any) {
|
||||||
const state = view.state
|
const state = view.state;
|
||||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
if (
|
||||||
return
|
lastState &&
|
||||||
}
|
lastState.doc.eq(state.doc) &&
|
||||||
|
lastState.selection.eq(state.selection)
|
||||||
|
)
|
||||||
|
{return;}
|
||||||
|
|
||||||
if (state.selection.empty) {
|
if (state.selection.empty) {
|
||||||
this.tooltip.style.display = 'none'
|
this.tooltip.style.display = "none";
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltip.style.display = ''
|
this.tooltip.style.display = "";
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection;
|
||||||
const start = view.coordsAtPos(from),
|
const start = view.coordsAtPos(from),
|
||||||
end = view.coordsAtPos(to)
|
end = view.coordsAtPos(to);
|
||||||
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
const box = this.tooltip.offsetParent.getBoundingClientRect();
|
||||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
const left = Math.max((start.left + end.left) / 2, start.left + 3);
|
||||||
this.tooltip.style.left = left - box.left + 'px'
|
this.tooltip.style.left = left - box.left + "px";
|
||||||
this.tooltip.style.bottom = box.bottom - (start.top + 15) + 'px'
|
this.tooltip.style.bottom = box.bottom - (start.top + 15) + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.tooltip.remove()
|
this.tooltip.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolTip(schema) {
|
export function toolTip(schema: any) {
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
view(editorView: EditorView) {
|
view(editorView: any) {
|
||||||
return new SelectionTooltip(editorView, schema)
|
return new SelectionTooltip(editorView, schema);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [...prev, toolTip(schema)]
|
plugins: (prev, schema) => [
|
||||||
|
...prev,
|
||||||
|
toolTip(schema)
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { inputRules } from 'prosemirror-inputrules'
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
import type { MarkType } from 'prosemirror-model'
|
import { MarkType } from 'prosemirror-model'
|
||||||
import { markInputRule } from './mark-input-rule'
|
import { markInputRule } from './mark-input-rule'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const strikethroughRule = (nodeType: MarkType) => markInputRule(/~{2}(.+)~{2}$/, nodeType, null)
|
const strikethroughRule = (nodeType: MarkType) => markInputRule(/(?:~~)(.+)(?:~~)$/, nodeType)
|
||||||
|
|
||||||
const strikethroughSchema = {
|
const strikethroughSchema = {
|
||||||
strikethrough: {
|
strikethrough: {
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { EditorState, Selection } from 'prosemirror-state'
|
import { EditorState, Selection } from 'prosemirror-state'
|
||||||
import type { Node, Schema, ResolvedPos, NodeSpec } from 'prosemirror-model'
|
import { Node, Schema, ResolvedPos } from 'prosemirror-model'
|
||||||
import { InputRule, inputRules } from 'prosemirror-inputrules'
|
import { InputRule, inputRules } from 'prosemirror-inputrules'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
import type OrderedMap from 'orderedmap'
|
|
||||||
|
|
||||||
export const tableInputRule = (schema: Schema) =>
|
export const tableInputRule = (schema: Schema) =>
|
||||||
new InputRule(
|
new InputRule(
|
||||||
new RegExp('^\\|{2,}\\s$'),
|
new RegExp('^\\|{2,}\\s$'),
|
||||||
(state: EditorState, match: string[], start: number, end: number) => {
|
(state: EditorState, match: string[], start: number, end: number) => {
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
const columns = [...Array.from({ length: match[0].trim().length - 1 })]
|
const columns = [...Array(match[0].trim().length - 1)]
|
||||||
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
||||||
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
||||||
const table = schema.node(schema.nodes.table, {}, [
|
const table = schema.node(schema.nodes.table, {}, [
|
||||||
|
@ -175,9 +174,8 @@ const getTextSize = (n: Node) => {
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
nodes: (prev.nodes as OrderedMap<NodeSpec>).append(tableSchema as NodeSpec)
|
nodes: (prev.nodes as any).append(tableSchema)
|
||||||
}),
|
}),
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
keymap({
|
keymap({
|
||||||
'Ctrl-Enter': (state, dispatch) => {
|
'Ctrl-Enter': (state, dispatch) => {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
|
import { DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
|
||||||
import { inputRules, wrappingInputRule } from 'prosemirror-inputrules'
|
import { EditorView } from 'prosemirror-view'
|
||||||
|
import { wrappingInputRule } from 'prosemirror-inputrules'
|
||||||
import { splitListItem } from 'prosemirror-schema-list'
|
import { splitListItem } from 'prosemirror-schema-list'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
import type { ProseMirrorExtension } from '../helpers'
|
import { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const todoListRule = (nodeType: NodeType) =>
|
const todoListRule = (nodeType: NodeType) =>
|
||||||
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
||||||
|
@ -59,9 +60,7 @@ class TodoItemView {
|
||||||
this.contentDOM = res.contentDOM
|
this.contentDOM = res.contentDOM
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos
|
this.getPos = getPos
|
||||||
;(this.dom as Element)
|
;(this.dom as Element).querySelector('input').onclick = this.handleClick.bind(this)
|
||||||
.querySelector('input')
|
|
||||||
.addEventListener('click', () => this.handleClick.bind(this))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e: MouseEvent) {
|
handleClick(e: MouseEvent) {
|
||||||
|
@ -88,8 +87,8 @@ export default (): ProseMirrorExtension => ({
|
||||||
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
||||||
],
|
],
|
||||||
nodeViews: {
|
nodeViews: {
|
||||||
todo_item: (node: any, view, getPos) => {
|
todo_item: (node, view, getPos) => {
|
||||||
return new TodoItemView(node, view, getPos)
|
return new TodoItemView(node, view, getPos)
|
||||||
}
|
}
|
||||||
} as any
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Plugin, EditorState } from 'prosemirror-state'
|
import { Plugin, EditorState } from 'prosemirror-state'
|
||||||
import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
import { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||||
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
import { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
||||||
|
|
||||||
export interface ProseMirrorExtension {
|
export interface ProseMirrorExtension {
|
||||||
schema?: (prev: SchemaSpec) => SchemaSpec
|
schema?: (prev: SchemaSpec) => SchemaSpec;
|
||||||
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
|
plugins?: (prev: Plugin[], schema: Schema) => Plugin[];
|
||||||
nodeViews?: { [key: string]: NodeViewFn }
|
nodeViews?: { [key: string]: NodeViewFn };
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProseMirrorState = EditorState | unknown
|
export type ProseMirrorState = EditorState | unknown
|
||||||
|
@ -21,7 +21,7 @@ export const isInitialized = (state: any) => state !== undefined && state instan
|
||||||
|
|
||||||
export const isEmpty = (state: any) =>
|
export const isEmpty = (state: any) =>
|
||||||
!isInitialized(state) ||
|
!isInitialized(state) ||
|
||||||
(state.doc.childCount === 1 &&
|
(state.doc.childCount == 1 &&
|
||||||
!state.doc.firstChild.type.spec.code &&
|
!state.doc.firstChild.type.spec.code &&
|
||||||
state.doc.firstChild.isTextblock &&
|
state.doc.firstChild.isTextblock &&
|
||||||
state.doc.firstChild.content.size === 0)
|
state.doc.firstChild.content.size == 0)
|
||||||
|
|
|
@ -1,52 +1,59 @@
|
||||||
// import menu from './extension/menu'
|
|
||||||
// import scroll from './prosemirror/extension/scroll'
|
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from './helpers'
|
import { ProseMirrorExtension } from './helpers'
|
||||||
import { Schema } from 'prosemirror-model'
|
import { Schema } from 'prosemirror-model'
|
||||||
import { t } from '../../../utils/intl'
|
|
||||||
import base from './extension/base'
|
import base from './extension/base'
|
||||||
import code from './extension/code'
|
|
||||||
import dragHandle from './extension/drag-handle'
|
|
||||||
import image from './extension/image'
|
|
||||||
import link from './extension/link'
|
|
||||||
import markdown from './extension/markdown'
|
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 pasteMarkdown from './extension/paste-markdown'
|
||||||
import table from './extension/table'
|
import table from './extension/table'
|
||||||
import collab from './extension/collab'
|
import collab from './extension/collab'
|
||||||
import type { Config, YOptions } from '../store/context'
|
import { Config, YOptions } from '../store/context'
|
||||||
import selectionMenu from './extension/selection'
|
import selectionMenu from './extension/selection'
|
||||||
import type { Command } from 'prosemirror-state'
|
|
||||||
import placeholder from './extension/placeholder'
|
|
||||||
import todoList from './extension/todo-list'
|
|
||||||
import strikethrough from './extension/strikethrough'
|
|
||||||
import scrollPlugin from './extension/scroll'
|
|
||||||
|
|
||||||
interface ExtensionsProps {
|
interface Props {
|
||||||
data?: unknown
|
data?: unknown;
|
||||||
keymap?: { [key: string]: Command }
|
keymap?: any;
|
||||||
config: Config
|
config: Config;
|
||||||
markdown: boolean
|
markdown: boolean;
|
||||||
path?: string
|
path?: string;
|
||||||
y?: YOptions
|
y?: YOptions;
|
||||||
schema?: Schema
|
schema?: Schema;
|
||||||
collab?: boolean
|
|
||||||
typewriterMode?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({
|
const customKeymap = (props: Props): ProseMirrorExtension => ({
|
||||||
plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev)
|
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]})
|
||||||
|
}
|
||||||
|
|
||||||
export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => {
|
return cmKeymap.of(keys)
|
||||||
const eee = [
|
}
|
||||||
placeholder(t('Just start typing...')),
|
*/
|
||||||
|
export const createExtensions = (props: Props): ProseMirrorExtension[] =>
|
||||||
|
props.markdown
|
||||||
|
? [
|
||||||
|
placeholder('Просто начните...'),
|
||||||
customKeymap(props),
|
customKeymap(props),
|
||||||
base(props.markdown),
|
base(props.markdown),
|
||||||
selectionMenu(),
|
collab(props.y),
|
||||||
scrollPlugin(props.config?.typewriterMode)
|
selectionMenu()
|
||||||
]
|
]
|
||||||
if (props.markdown) {
|
: [
|
||||||
eee.push(
|
selectionMenu(),
|
||||||
|
customKeymap(props),
|
||||||
|
base(props.markdown),
|
||||||
markdown(),
|
markdown(),
|
||||||
todoList(),
|
todoList(),
|
||||||
dragHandle(),
|
dragHandle(),
|
||||||
|
@ -55,7 +62,9 @@ export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[]
|
||||||
link(),
|
link(),
|
||||||
table(),
|
table(),
|
||||||
image(props.path),
|
image(props.path),
|
||||||
pasteMarkdown()
|
pasteMarkdown(),
|
||||||
|
collab(props.y)
|
||||||
|
// scroll(props.config.typewriterMode),
|
||||||
/*
|
/*
|
||||||
codeBlock({
|
codeBlock({
|
||||||
theme: codeTheme(props.config),
|
theme: codeTheme(props.config),
|
||||||
|
@ -65,11 +74,7 @@ export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[]
|
||||||
extensions: () => [codeMirrorKeymap(props)],
|
extensions: () => [codeMirrorKeymap(props)],
|
||||||
}),
|
}),
|
||||||
*/
|
*/
|
||||||
)
|
]
|
||||||
}
|
|
||||||
if (props.collab) eee.push(collab(props.y))
|
|
||||||
return eee
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createEmptyText = () => ({
|
export const createEmptyText = () => ({
|
||||||
doc: {
|
doc: {
|
||||||
|
@ -83,7 +88,7 @@ export const createEmptyText = () => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createSchema = (props: ExtensionsProps) => {
|
export const createSchema = (props: Props) => {
|
||||||
const extensions = createExtensions({
|
const extensions = createExtensions({
|
||||||
config: props.config,
|
config: props.config,
|
||||||
markdown: props.markdown,
|
markdown: props.markdown,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import { serialize } from './markdown'
|
import { serialize } from './markdown'
|
||||||
|
|
||||||
export const copy = async (text: string): Promise<void> => {
|
export const copy = async (text: string): Promise<void> => {
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
import { Store, createStore, unwrap } from 'solid-js/store'
|
import { Store, createStore, unwrap } from 'solid-js/store'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import type { Command, EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import { undo, redo } from 'prosemirror-history'
|
import { undo, redo } from 'prosemirror-history'
|
||||||
import { selectAll, deleteSelection } from 'prosemirror-commands'
|
import { selectAll, deleteSelection } from 'prosemirror-commands'
|
||||||
|
import * as Y from 'yjs'
|
||||||
import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
|
import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
|
||||||
import debounce from 'lodash/debounce'
|
import { WebrtcProvider } from 'y-webrtc'
|
||||||
|
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
||||||
import { State, Draft, Config, ServiceError, newState } from './context'
|
import { State, File, Config, ServiceError, newState } from './context'
|
||||||
|
import { mod } from '../env'
|
||||||
import { serialize, createMarkdownParser } from '../markdown'
|
import { serialize, createMarkdownParser } from '../markdown'
|
||||||
import db from '../db'
|
import db from '../db'
|
||||||
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
||||||
import { createSignal } from 'solid-js'
|
import { Awareness } from 'y-protocols/awareness'
|
||||||
|
|
||||||
const isText = (x) => x && x.doc && x.selection
|
const isText = (x: any) => x && x.doc && x.selection
|
||||||
const isDraft = (x): boolean => x && (x.text || x.path)
|
const isState = (x: any) => typeof x.lastModified !== 'string' && Array.isArray(x.files)
|
||||||
const mod = 'Ctrl'
|
const isFile = (x: any): boolean => x && (x.text || x.path)
|
||||||
|
|
||||||
export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
export const createCtrl = (initial: State): [Store<State>, any] => {
|
||||||
const [store, setState] = createStore(initial)
|
const [store, setState] = createStore(initial)
|
||||||
|
|
||||||
const onNew = () => {
|
|
||||||
newDraft()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDiscard = () => {
|
const onDiscard = () => {
|
||||||
discard()
|
discard()
|
||||||
return true
|
return true
|
||||||
|
@ -32,14 +31,19 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
const onToggleMarkdown = () => toggleMarkdown()
|
const onToggleMarkdown = () => toggleMarkdown()
|
||||||
|
|
||||||
const onUndo = () => {
|
const onUndo = () => {
|
||||||
if (!isInitialized(store.text as EditorState)) return
|
if (!isInitialized(store.text)) return
|
||||||
const text = store.text as EditorState
|
const text = store.text as EditorState
|
||||||
store.collab?.started ? yUndo(text) : undo(text, store.editorView.dispatch)
|
if (store.collab?.started) {
|
||||||
|
yUndo(text)
|
||||||
|
} else {
|
||||||
|
undo(text, store.editorView.dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRedo = () => {
|
const onRedo = () => {
|
||||||
if (!isInitialized(store.text as EditorState)) return
|
if (!isInitialized(store.text)) return
|
||||||
const text = store.text as EditorState
|
const text = store.text as EditorState
|
||||||
if (store.collab?.started) {
|
if (store.collab?.started) {
|
||||||
yRedo(text)
|
yRedo(text)
|
||||||
|
@ -51,59 +55,53 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
[`${mod}-n`]: onNew,
|
|
||||||
[`${mod}-w`]: onDiscard,
|
[`${mod}-w`]: onDiscard,
|
||||||
[`${mod}-z`]: onUndo,
|
[`${mod}-z`]: onUndo,
|
||||||
[`Shift-${mod}-z`]: onRedo,
|
[`Shift-${mod}-z`]: onRedo,
|
||||||
[`${mod}-y`]: onRedo,
|
[`${mod}-y`]: onRedo,
|
||||||
[`${mod}-m`]: onToggleMarkdown
|
[`${mod}-m`]: onToggleMarkdown
|
||||||
} as { [key: string]: Command }
|
|
||||||
|
|
||||||
const createTextFromDraft = async (d: Draft): Promise<Draft> => {
|
|
||||||
let draft = d
|
|
||||||
const state = unwrap(store)
|
|
||||||
if (draft.path) {
|
|
||||||
draft = await loadDraft(state.config, draft.path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createTextFromFile = async (file: File) => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
|
||||||
const extensions = createExtensions({
|
const extensions = createExtensions({
|
||||||
config: state.config,
|
config: state.config,
|
||||||
markdown: draft.markdown,
|
markdown: file.markdown,
|
||||||
path: draft.path,
|
path: file.path,
|
||||||
keymap
|
keymap
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: draft.text,
|
text: file.text,
|
||||||
extensions,
|
extensions,
|
||||||
lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined,
|
lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
|
||||||
path: draft.path,
|
path: file.path,
|
||||||
markdown: draft.markdown
|
markdown: file.markdown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
const addToFiles = (files: File[], prev: State) => {
|
||||||
const addToDrafts = (drafts: Draft[], prev: Draft) => {
|
const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
|
||||||
const text = prev.path ? undefined : JSON.stringify(prev.text)
|
|
||||||
return [
|
return [
|
||||||
...drafts,
|
...files,
|
||||||
{
|
{
|
||||||
body: text,
|
text,
|
||||||
lastModified: prev.lastModified,
|
lastModified: prev.lastModified?.toISOString(),
|
||||||
path: prev.path,
|
path: prev.path,
|
||||||
markdown: prev.markdown
|
markdown: prev.markdown
|
||||||
} as Draft
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const discardText = async () => {
|
const discardText = async () => {
|
||||||
const state = unwrap(store)
|
const state = unwrap(store)
|
||||||
const index = state.drafts.length - 1
|
const index = state.files.length - 1
|
||||||
const draft = index !== -1 ? state.drafts[index] : undefined
|
const file = index !== -1 ? state.files[index] : undefined
|
||||||
|
|
||||||
let next
|
let next: Partial<State>
|
||||||
if (draft) {
|
if (file) {
|
||||||
next = await createTextFromDraft(draft)
|
next = await createTextFromFile(file)
|
||||||
} else {
|
} else {
|
||||||
const extensions = createExtensions({
|
const extensions = createExtensions({
|
||||||
config: state.config ?? store.config,
|
config: state.config ?? store.config,
|
||||||
|
@ -114,35 +112,46 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
next = {
|
next = {
|
||||||
text: createEmptyText(),
|
text: createEmptyText(),
|
||||||
extensions,
|
extensions,
|
||||||
lastModified: new Date(),
|
lastModified: undefined,
|
||||||
path: undefined,
|
path: undefined,
|
||||||
markdown: state.markdown
|
markdown: state.markdown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const drafts = state.drafts.filter((f: Draft) => f !== draft)
|
const files = state.files.filter((f: File) => f !== file)
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
drafts,
|
files,
|
||||||
...next,
|
...next,
|
||||||
collab: state.collab,
|
collab: file ? undefined : state.collab,
|
||||||
error: undefined
|
error: undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const readStoredState = async (): Promise<State> => {
|
const fetchData = async (): Promise<State> => {
|
||||||
const state: State = unwrap(store)
|
const state: State = unwrap(store)
|
||||||
const room = window.location.pathname?.slice(1).trim()
|
const room = window.location.pathname?.slice(1).trim()
|
||||||
const args = { draft: room }
|
const args = { room: room ? room : undefined }
|
||||||
const data = await db.get('state')
|
const data = await db.get('state')
|
||||||
|
let parsed: any
|
||||||
if (data !== undefined) {
|
if (data !== undefined) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data)
|
parsed = JSON.parse(data)
|
||||||
|
} catch (err) {
|
||||||
|
throw new ServiceError('invalid_state', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
return { ...state, args }
|
||||||
|
}
|
||||||
|
|
||||||
let text = state.text
|
let text = state.text
|
||||||
if (parsed.text) {
|
if (parsed.text) {
|
||||||
if (!isText(parsed.text)) {
|
if (!isText(parsed.text)) {
|
||||||
throw new ServiceError('invalid_state', parsed.text)
|
throw new ServiceError('invalid_state', parsed.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
text = parsed.text
|
text = parsed.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,34 +162,39 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
config: undefined
|
config: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const draft of parsed.drafts || []) {
|
const newState = {
|
||||||
if (!isDraft(draft)) {
|
|
||||||
console.error('[editor] invalid draft', draft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...parsed,
|
...parsed,
|
||||||
text,
|
text,
|
||||||
extensions,
|
extensions,
|
||||||
// config,
|
// config,
|
||||||
args,
|
args
|
||||||
lastModified: new Date(parsed.lastModified)
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
if (newState.lastModified) {
|
||||||
return { ...state, args }
|
newState.lastModified = new Date(newState.lastModified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const file of parsed.files) {
|
||||||
|
if (!isFile(file)) {
|
||||||
|
throw new ServiceError('invalid_file', file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTheme = (state: State) => ({ theme: state.config?.theme || '' })
|
if (!isState(newState)) {
|
||||||
|
throw new ServiceError('invalid_state', newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTheme = (state: State) => ({ theme: state.config.theme })
|
||||||
|
|
||||||
const clean = () => {
|
const clean = () => {
|
||||||
setState({
|
setState({
|
||||||
...newState(),
|
...newState(),
|
||||||
loading: 'initialized',
|
loading: 'initialized',
|
||||||
drafts: [],
|
files: [],
|
||||||
|
fullscreen: store.fullscreen,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
text: undefined
|
text: undefined
|
||||||
|
@ -190,7 +204,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
const discard = async () => {
|
const discard = async () => {
|
||||||
if (store.path) {
|
if (store.path) {
|
||||||
await discardText()
|
await discardText()
|
||||||
} else if (store.drafts.length > 0 && isEmpty(store.text as EditorState)) {
|
} else if (store.files.length > 0 && isEmpty(store.text)) {
|
||||||
await discardText()
|
await discardText()
|
||||||
} else {
|
} else {
|
||||||
selectAll(store.editorView.state, store.editorView.dispatch)
|
selectAll(store.editorView.state, store.editorView.dispatch)
|
||||||
|
@ -199,128 +213,34 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
let state = await readStoredState()
|
let data = await fetchData()
|
||||||
console.log('[editor] init with state', state)
|
|
||||||
try {
|
try {
|
||||||
if (state.args?.room) {
|
if (data.args.room) {
|
||||||
state = await doStartCollab(state)
|
data = await doStartCollab(data)
|
||||||
} else if (state.args.text) {
|
} else if (!data.text) {
|
||||||
state = await doOpenDraft(state, {
|
|
||||||
text: { ...JSON.parse(state.args.text) },
|
|
||||||
lastModified: new Date()
|
|
||||||
})
|
|
||||||
} else if (state.args.draft) {
|
|
||||||
const draft = await loadDraft(state.config, state.args.draft)
|
|
||||||
state = await doOpenDraft(state, draft)
|
|
||||||
} else if (state.path) {
|
|
||||||
const draft = await loadDraft(state.config, state.path)
|
|
||||||
state = await doOpenDraft(state, draft)
|
|
||||||
} else if (!state.text) {
|
|
||||||
const text = createEmptyText()
|
const text = createEmptyText()
|
||||||
const extensions = createExtensions({
|
const extensions = createExtensions({
|
||||||
config: state.config ?? store.config,
|
config: data.config ?? store.config,
|
||||||
markdown: state.markdown ?? store.markdown,
|
markdown: data.markdown ?? store.markdown,
|
||||||
keymap: keymap
|
keymap: keymap
|
||||||
})
|
})
|
||||||
state = { ...state, text, extensions }
|
data = { ...data, text, extensions }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
state = { ...state, error: error.errorObject }
|
data = { ...data, error: error.errorObject }
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
...state,
|
...data,
|
||||||
config: { ...state.config, ...getTheme(state) },
|
config: { ...data.config, ...getTheme(data) },
|
||||||
loading: 'initialized'
|
loading: 'initialized'
|
||||||
})
|
})
|
||||||
console.log('[editor] initialized successfully', state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDraft = async (config: Config, path: string): Promise<Draft> => {
|
const saveState = () => debounce(async (state: State) => {
|
||||||
const [draft, setDraft] = createSignal<Draft>()
|
const data: any = {
|
||||||
const schema = createSchema({
|
|
||||||
config,
|
|
||||||
markdown: false,
|
|
||||||
path,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
const parser = createMarkdownParser(schema)
|
|
||||||
return {
|
|
||||||
...draft(),
|
|
||||||
text: {
|
|
||||||
doc: parser.parse(draft().body).toJSON(),
|
|
||||||
selection: {
|
|
||||||
type: 'text',
|
|
||||||
anchor: 1,
|
|
||||||
head: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDraft = () => {
|
|
||||||
if (isEmpty(store.text as EditorState) && !store.path) return
|
|
||||||
const state = unwrap(store)
|
|
||||||
let drafts = state.drafts
|
|
||||||
if (!state.error) {
|
|
||||||
drafts = addToDrafts(drafts, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config ?? store.config,
|
|
||||||
markdown: state.markdown ?? store.markdown,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
setState({
|
|
||||||
text: createEmptyText(),
|
|
||||||
extensions,
|
|
||||||
drafts,
|
|
||||||
lastModified: undefined,
|
|
||||||
path: undefined,
|
|
||||||
error: undefined,
|
|
||||||
collab: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDraft = async (draft: Draft) => {
|
|
||||||
const state: State = unwrap(store)
|
|
||||||
const update = await doOpenDraft(state, draft)
|
|
||||||
setState(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
const doOpenDraft = async (state: State, draft: Draft): Promise<State> => {
|
|
||||||
const findIndexOfDraft = (f: Draft) => {
|
|
||||||
for (let i = 0; i < state.drafts.length; i++) {
|
|
||||||
if (state.drafts[i] === f || (f.path && state.drafts[i].path === f.path)) return i
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
const index = findIndexOfDraft(draft)
|
|
||||||
const item = index === -1 ? draft : state.drafts[index]
|
|
||||||
let drafts = state.drafts.filter((d: Draft) => d !== item)
|
|
||||||
if (!isEmpty(state.text as EditorState) && state.lastModified) {
|
|
||||||
drafts = addToDrafts(drafts, { lastModified: new Date(), text: state.text } as Draft)
|
|
||||||
}
|
|
||||||
draft.lastModified = item.lastModified
|
|
||||||
const next = await createTextFromDraft(draft)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...next,
|
|
||||||
drafts,
|
|
||||||
collab: undefined,
|
|
||||||
error: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveState = () =>
|
|
||||||
debounce(async (state: State) => {
|
|
||||||
const data: State = {
|
|
||||||
loading: 'initialized',
|
|
||||||
lastModified: state.lastModified,
|
lastModified: state.lastModified,
|
||||||
drafts: state.drafts,
|
files: state.files,
|
||||||
config: state.config,
|
config: state.config,
|
||||||
path: state.path,
|
path: state.path,
|
||||||
markdown: state.markdown,
|
markdown: state.markdown,
|
||||||
|
@ -329,14 +249,8 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInitialized(state.text as EditorState)) {
|
if (isInitialized(state.text)) {
|
||||||
if (state.path) {
|
|
||||||
const text = serialize(store.editorView.state)
|
|
||||||
// TODO: saving draft logix here
|
|
||||||
// await remote.writeDraft(state.path, text)
|
|
||||||
} else {
|
|
||||||
data.text = store.editorView.state.toJSON()
|
data.text = store.editorView.state.toJSON()
|
||||||
}
|
|
||||||
} else if (state.text) {
|
} else if (state.text) {
|
||||||
data.text = state.text
|
data.text = state.text
|
||||||
}
|
}
|
||||||
|
@ -344,18 +258,21 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
db.set('state', JSON.stringify(data))
|
db.set('state', JSON.stringify(data))
|
||||||
}, 200)
|
}, 200)
|
||||||
|
|
||||||
const startCollab = () => {
|
const setFullscreen = (fullscreen: boolean) => {
|
||||||
|
setState({ fullscreen })
|
||||||
|
}
|
||||||
|
|
||||||
|
const startCollab = async () => {
|
||||||
const state: State = unwrap(store)
|
const state: State = unwrap(store)
|
||||||
const update = doStartCollab(state)
|
const update = await doStartCollab(state)
|
||||||
setState(update)
|
setState(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
const doStartCollab = async (state: State): Promise<State> => {
|
const doStartCollab = async (state: State): Promise<State> => {
|
||||||
const restoredRoom = state.args?.room && state.collab?.room !== state.args.room
|
const backup = state.args?.room && state.collab?.room !== state.args.room
|
||||||
const room = state.args?.room ?? uuidv4()
|
const room = state.args?.room ?? uuidv4()
|
||||||
state.args = { ...state.args, room }
|
window.history.replaceState(null, '', `/${room}`)
|
||||||
let newst = state
|
|
||||||
try {
|
|
||||||
const { roomConnect } = await import('../prosemirror/p2p')
|
const { roomConnect } = await import('../prosemirror/p2p')
|
||||||
const [type, provider] = roomConnect(room)
|
const [type, provider] = roomConnect(room)
|
||||||
|
|
||||||
|
@ -364,38 +281,30 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
markdown: state.markdown,
|
markdown: state.markdown,
|
||||||
path: state.path,
|
path: state.path,
|
||||||
keymap,
|
keymap,
|
||||||
y: { type, provider },
|
y: { type, provider }
|
||||||
collab: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if ((restoredRoom && !isEmpty(state.text as EditorState)) || state.path) {
|
let newState = state
|
||||||
let drafts = state.drafts
|
if ((backup && !isEmpty(state.text)) || state.path) {
|
||||||
|
let files = state.files
|
||||||
if (!state.error) {
|
if (!state.error) {
|
||||||
drafts = addToDrafts(drafts, { lastModified: new Date(), text: state.text } as Draft)
|
files = addToFiles(files, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
newst = {
|
newState = {
|
||||||
...state,
|
...state,
|
||||||
drafts,
|
files,
|
||||||
lastModified: undefined,
|
lastModified: undefined,
|
||||||
path: undefined,
|
path: undefined,
|
||||||
error: undefined
|
error: undefined
|
||||||
}
|
}
|
||||||
window.history.replaceState(null, '', `/${room}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...newst,
|
...newState,
|
||||||
extensions,
|
extensions,
|
||||||
collab: { started: true, room, y: { type, provider } }
|
collab: { started: true, room, y: { type, provider } }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
collab: { error }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopCollab = (state: State) => {
|
const stopCollab = (state: State) => {
|
||||||
|
@ -404,8 +313,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
config: state.config,
|
config: state.config,
|
||||||
markdown: state.markdown,
|
markdown: state.markdown,
|
||||||
path: state.path,
|
path: state.path,
|
||||||
keymap,
|
keymap
|
||||||
collab: false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setState({ collab: undefined, extensions })
|
setState({ collab: undefined, extensions })
|
||||||
|
@ -417,7 +325,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
const editorState = store.text as EditorState
|
const editorState = store.text as EditorState
|
||||||
const markdown = !state.markdown
|
const markdown = !state.markdown
|
||||||
const selection = { type: 'text', anchor: 1, head: 1 }
|
const selection = { type: 'text', anchor: 1, head: 1 }
|
||||||
let doc
|
let doc: any
|
||||||
|
|
||||||
if (markdown) {
|
if (markdown) {
|
||||||
const lines = serialize(editorState).split('\n')
|
const lines = serialize(editorState).split('\n')
|
||||||
|
@ -457,7 +365,6 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
extensions,
|
extensions,
|
||||||
markdown
|
markdown
|
||||||
})
|
})
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfig = (config: Partial<Config>) => {
|
const updateConfig = (config: Partial<Config>) => {
|
||||||
|
@ -491,10 +398,8 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
||||||
discard,
|
discard,
|
||||||
getTheme,
|
getTheme,
|
||||||
init,
|
init,
|
||||||
loadDraft,
|
|
||||||
newDraft,
|
|
||||||
openDraft,
|
|
||||||
saveState,
|
saveState,
|
||||||
|
setFullscreen,
|
||||||
setState,
|
setState,
|
||||||
startCollab,
|
startCollab,
|
||||||
stopCollab,
|
stopCollab,
|
||||||
|
|
|
@ -1,84 +1,79 @@
|
||||||
import { createContext, useContext } from 'solid-js'
|
import { createContext, useContext } from 'solid-js'
|
||||||
import type { Store } from 'solid-js/store'
|
import { Store } from 'solid-js/store'
|
||||||
import type { XmlFragment } from 'yjs'
|
import { XmlFragment } from 'yjs'
|
||||||
import type { WebrtcProvider } from 'y-webrtc'
|
import { WebrtcProvider } from 'y-webrtc'
|
||||||
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
import { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
|
||||||
|
|
||||||
export interface Args {
|
export interface Args {
|
||||||
cwd?: string
|
cwd?: string;
|
||||||
draft?: string
|
file?: string;
|
||||||
room?: string
|
room?: string;
|
||||||
text?: string
|
text?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrettierConfig {
|
export interface PrettierConfig {
|
||||||
printWidth: number
|
printWidth: number;
|
||||||
tabWidth: number
|
tabWidth: number;
|
||||||
useTabs: boolean
|
useTabs: boolean;
|
||||||
semi: boolean
|
semi: boolean;
|
||||||
singleQuote: boolean
|
singleQuote: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
theme: string
|
theme: string;
|
||||||
// codeTheme: string;
|
// codeTheme: string;
|
||||||
// alwaysOnTop: boolean;
|
font: string;
|
||||||
font: string
|
fontSize: number;
|
||||||
fontSize: number
|
contentWidth: number;
|
||||||
contentWidth: number
|
typewriterMode: boolean;
|
||||||
typewriterMode?: boolean;
|
prettier: PrettierConfig;
|
||||||
prettier: PrettierConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorObject {
|
export interface ErrorObject {
|
||||||
id: string
|
id: string;
|
||||||
props?: any
|
props?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YOptions {
|
export interface YOptions {
|
||||||
type: XmlFragment
|
type: XmlFragment;
|
||||||
provider: WebrtcProvider
|
provider: WebrtcProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Collab {
|
export interface Collab {
|
||||||
started?: boolean
|
started?: boolean;
|
||||||
error?: boolean
|
error?: boolean;
|
||||||
room?: string
|
room?: string;
|
||||||
y?: YOptions
|
y?: YOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoadingType = 'loading' | 'initialized'
|
export type LoadingType = 'loading' | 'initialized'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
text?: ProseMirrorState
|
text?: ProseMirrorState;
|
||||||
editorView?: EditorView
|
editorView?: any;
|
||||||
extensions?: ProseMirrorExtension[]
|
extensions?: ProseMirrorExtension[];
|
||||||
markdown?: boolean
|
markdown?: boolean;
|
||||||
lastModified?: Date
|
lastModified?: Date;
|
||||||
drafts: Draft[]
|
files: File[];
|
||||||
config: Config
|
config: Config;
|
||||||
error?: ErrorObject
|
error?: ErrorObject;
|
||||||
loading: LoadingType
|
loading: LoadingType;
|
||||||
fullscreen?: boolean
|
fullscreen: boolean;
|
||||||
collab?: Collab
|
collab?: Collab;
|
||||||
path?: string
|
path?: string;
|
||||||
args?: Args
|
args?: Args;
|
||||||
isMac?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Draft {
|
export interface File {
|
||||||
text?: { [key: string]: any }
|
text?: { [key: string]: any };
|
||||||
body?: string
|
lastModified?: string;
|
||||||
lastModified?: Date
|
path?: string;
|
||||||
path?: string
|
markdown?: boolean;
|
||||||
markdown?: boolean
|
|
||||||
extensions?: ProseMirrorExtension[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceError extends Error {
|
export class ServiceError extends Error {
|
||||||
public errorObject: ErrorObject
|
public errorObject: ErrorObject
|
||||||
constructor(id: string, props: any) {
|
constructor(id: string, props: unknown) {
|
||||||
super(id)
|
super(id)
|
||||||
this.errorObject = { id, props }
|
this.errorObject = { id, props }
|
||||||
}
|
}
|
||||||
|
@ -90,7 +85,7 @@ export const useState = () => useContext(StateContext)
|
||||||
|
|
||||||
export const newState = (props: Partial<State> = {}): State => ({
|
export const newState = (props: Partial<State> = {}): State => ({
|
||||||
extensions: [],
|
extensions: [],
|
||||||
drafts: [],
|
files: [],
|
||||||
loading: 'loading',
|
loading: 'loading',
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
markdown: false,
|
markdown: false,
|
||||||
|
@ -100,7 +95,7 @@ export const newState = (props: Partial<State> = {}): State => ({
|
||||||
font: 'muller',
|
font: 'muller',
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
contentWidth: 800,
|
contentWidth: 800,
|
||||||
// typewriterMode: true,
|
typewriterMode: true,
|
||||||
prettier: {
|
prettier: {
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
|
|
||||||
.article__title {
|
.article__title {
|
||||||
@include font-size(2.4rem);
|
@include font-size(2.4rem);
|
||||||
|
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +35,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@include font-size(1.4rem);
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
.error {
|
|
||||||
button {
|
button {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
@ -9,13 +8,12 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: Muller;
|
font-family: 'JetBrains Mono';
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
background: none;
|
background: none;
|
||||||
|
font-family: 'Muller';
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border: 1px solid var(--foreground);
|
border: 1px solid var(--foreground);
|
||||||
}
|
}
|
||||||
|
@ -25,4 +23,3 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
background: var(--primary-background);
|
background: var(--primary-background);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +1,9 @@
|
||||||
@import './Button';
|
|
||||||
@import './Sidebar';
|
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
margin: 0.5em;
|
padding-top: 1em;
|
||||||
padding: 1em;
|
|
||||||
min-width: 50%;
|
|
||||||
min-height: fit-content;
|
|
||||||
display: inline-block;
|
|
||||||
border: 1px dashed rgb(0 0 0 / 80%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror {
|
|
||||||
a {
|
a {
|
||||||
color: rgb(0 100 200);
|
color: rgb(0, 100, 200);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
color: rgb(0 100 200 / 70%);
|
color: rgb(0, 80, 160);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
@ -34,8 +25,9 @@
|
||||||
textarea {
|
textarea {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
-webkit-padding: 0.4em 0;
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
margin: 0 0 0.5em;
|
margin: 0 0 0.5em 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -63,11 +55,13 @@
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
position: relative;
|
position: relative;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
-webkit-font-variant-ligatures: none;
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
margin: 1em 1em 1em 2em;
|
margin: 1em 1em 1em 2em;
|
||||||
|
@ -120,17 +114,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
@include font-size(1.6rem);
|
|
||||||
|
|
||||||
margin: 1.5em 0;
|
|
||||||
border-left: 2px solid;
|
border-left: 2px solid;
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
margin: 1.5em 0;
|
||||||
padding-left: 1.6em;
|
padding-left: 1.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menuitem {
|
.ProseMirror-menuitem {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
> * {
|
> * {
|
||||||
|
@ -160,10 +152,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-tooltip .ProseMirror-menu {
|
.ProseMirror-tooltip .ProseMirror-menu {
|
||||||
|
width: -webkit-fit-content;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror-menuitem {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror-menuseparator {
|
.ProseMirror-menuseparator {
|
||||||
border-right: 1px solid #ddd;
|
border-right: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
@ -187,11 +184,11 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown::after {
|
.ProseMirror-menu-dropdown:after {
|
||||||
content: '';
|
content: '';
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
border-right: 4px solid transparent;
|
border-right: 4px solid transparent;
|
||||||
border-top: 4px solid draftcurrentcolor;
|
border-top: 4px solid currentColor;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
@ -209,7 +206,6 @@
|
||||||
|
|
||||||
.ProseMirror-menu-dropdown-menu {
|
.ProseMirror-menu-dropdown-menu {
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
|
|
||||||
/* min-width: 6em; */
|
/* min-width: 6em; */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,11 +223,11 @@
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-menu-submenu-label::after {
|
.ProseMirror-menu-submenu-label:after {
|
||||||
content: '';
|
content: '';
|
||||||
border-top: 4px solid transparent;
|
border-top: 4px solid transparent;
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 4px solid transparent;
|
||||||
border-left: 4px solid draftcurrentcolor;
|
border-left: 4px solid currentColor;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
|
@ -272,6 +268,7 @@
|
||||||
border-bottom: 1px solid silver;
|
border-bottom: 1px solid silver;
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
@ -286,7 +283,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ProseMirror-icon svg {
|
.ProseMirror-icon svg {
|
||||||
fill: draftcurrentcolor;
|
fill: currentColor;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,6 +303,10 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::-moz-selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.ProseMirror-hideselection {
|
.ProseMirror-hideselection {
|
||||||
caret-color: transparent;
|
caret-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -319,7 +320,7 @@ li.ProseMirror-selectednode {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.ProseMirror-selectednode::after {
|
li.ProseMirror-selectednode:after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -32px;
|
left: -32px;
|
||||||
|
@ -349,7 +350,7 @@ li.ProseMirror-selectednode::after {
|
||||||
|
|
||||||
.ProseMirror-prompt {
|
.ProseMirror-prompt {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 4px 10px rgba(0 0 0 / 25%);
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -394,7 +395,7 @@ li.ProseMirror-selectednode::after {
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
box-shadow: 0 4px 10px rgba(0 0 0 / 25%);
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||||
color: #000;
|
color: #000;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +17,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.error pre {
|
.error pre {
|
||||||
background: var(--foreground);
|
background: var(--foreground) 19;
|
||||||
border: 1px solid var(--foreground);
|
border: 1px solid var(--foreground);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|
3
src/components/Editor/styles/Index.scss
Normal file
3
src/components/Editor/styles/Index.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.index {
|
||||||
|
width: 350px;
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
.layout--editor {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: Muller;
|
font-family: 'Muller';
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border-color: var(--background);
|
border-color: var(--background);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin-top: -2.2rem !important;
|
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
.sidebar-container {
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
font-family: 'Muller';
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-off {
|
.sidebar-off {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -31,7 +44,7 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&:after {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cmask id='mask0_1090_23825' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='0' y='14' width='4' height='4'%3E%3Crect y='14.8237' width='3.17647' height='3.17647' fill='%23fff'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_1090_23825)'%3E%3Cpath d='M16.0941 1.05908H0.847027C0.379194 1.05908 0 1.43828 0 1.90611V18.0003L3.38824 14.612H16.0942C16.562 14.612 16.9412 14.2328 16.9412 13.765V1.90614C16.9412 1.43831 16.562 1.05912 16.0942 1.05912L16.0941 1.05908ZM15.2471 12.9179H1.69412V2.7532H15.2471V12.9179Z' fill='black'/%3E%3C/g%3E%3Crect x='1' y='1' width='16' height='12.8235' stroke='black' stroke-width='2'/%3E%3Crect x='4.23535' y='3.17627' width='9.52941' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='9.5293' width='7.41176' height='2.11765' fill='black'/%3E%3Crect x='4.23535' y='6.35303' width='5.29412' height='2.11765' fill='black'/%3E%3C/svg%3E");
|
||||||
content: '';
|
content: '';
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
@ -60,8 +73,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-label {
|
.sidebar-label {
|
||||||
color: var(--foreground);
|
color: var(--foreground) 7f;
|
||||||
|
|
||||||
> i {
|
> i {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
@ -71,7 +83,6 @@
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container button,
|
.sidebar-container button,
|
||||||
.sidebar-container a,
|
.sidebar-container a,
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
|
@ -81,7 +92,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-family: Muller;
|
font-family: 'Muller';
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container a,
|
.sidebar-container a,
|
||||||
|
@ -92,22 +104,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
@include font-size(1.6rem);
|
|
||||||
|
|
||||||
color: rgb(255 255 255 / 50%);
|
|
||||||
font-family: Muller;
|
|
||||||
display: inline-flex;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
@include font-size(120%);
|
@include font-size(120%);
|
||||||
|
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,12 +138,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
color: var(--foreground);
|
color: var(--foreground) 99;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.file {
|
&.file {
|
||||||
color: rgb(255 255 255 / 50%);
|
color: rgba(255,255,255,0.5);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0 0 1em 1.5em;
|
margin: 0 0 1em 1.5em;
|
||||||
width: calc(100% - 2rem);
|
width: calc(100% - 2rem);
|
||||||
|
@ -178,22 +176,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-switcher {
|
.theme-switcher {
|
||||||
border-bottom: 1px solid rgb(255 255 255 / 30%);
|
border-bottom: 1px solid rgba(255,255,255,0.3);
|
||||||
border-top: 1px solid rgb(255 255 255 / 30%);
|
border-top: 1px solid rgba(255,255,255,0.3);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
|
|
||||||
input[type='checkbox'] {
|
input[type=checkbox] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
+ label {
|
+ label {
|
||||||
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A")
|
background: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.20869 7.73227C5.22953 7.36499 4.38795 6.70402 3.79906 5.83976C3.2103 4.97565 2.90318 3.95064 2.91979 2.90512C2.93639 1.8597 3.27597 0.844915 3.8919 0C2.82862 0.254038 1.87585 0.844877 1.17594 1.68438C0.475894 2.52388 0.0660276 3.5671 0.00731938 4.6585C-0.0513888 5.74989 0.244296 6.83095 0.850296 7.74073C1.45631 8.65037 2.34006 9.33992 3.36994 9.70637C4.39987 10.073 5.52063 10.0969 6.56523 9.77466C7.60985 9.45247 8.52223 8.80134 9.16667 7.91837C8.1842 8.15404 7.15363 8.08912 6.20869 7.73205V7.73227Z' fill='white'/%3E%3C/svg%3E%0A") no-repeat 30px 9px,
|
||||||
no-repeat 30px 9px,
|
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A") #000 no-repeat 8px 8px;
|
||||||
url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.41196 0H5.58811V2.43024H6.41196V0ZM5.99988 8.96576C4.36601 8.96576 3.03419 7.63397 3.03419 6.00007C3.04792 4.3662 4.36598 3.04818 5.99988 3.03439C7.63375 3.03439 8.96557 4.3662 8.96557 6.00007C8.96557 7.63395 7.63375 8.96576 5.99988 8.96576ZM5.58811 9.56977H6.41196V12H5.58811V9.56977ZM12.0002 5.58811H9.56996V6.41196H12.0002V5.58811ZM0 5.58811H2.43024V6.41196H0V5.58811ZM8.81339 3.76727L10.5318 2.04891L9.94925 1.46641L8.23089 3.18477L8.81339 3.76727ZM3.7745 8.8129L2.05614 10.5313L1.47364 9.94877L3.192 8.2304L3.7745 8.8129ZM9.95043 10.5269L10.5329 9.94437L8.81456 8.22601L8.23207 8.80851L9.95043 10.5269ZM3.76864 3.18731L3.18614 3.76981L1.46778 2.05145L2.05028 1.46895L3.76864 3.18731Z' fill='%231F1F1F'/%3E%3C/svg%3E%0A")
|
|
||||||
#000 no-repeat 8px 8px;
|
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -204,7 +200,7 @@
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
|
|
||||||
&::before {
|
&:before {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -220,7 +216,7 @@
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
||||||
&::before {
|
&:before {
|
||||||
background-color: #1f1f1f;
|
background-color: #1f1f1f;
|
||||||
left: 24px;
|
left: 24px;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user