demo
This commit is contained in:
parent
1d7a71ae3a
commit
642d8b9dd1
|
@ -27,6 +27,7 @@ module.exports = {
|
|||
// 'plugin:@typescript-eslint/recommended-requiring-type-checking'
|
||||
],
|
||||
rules: {
|
||||
'no-nested-ternary': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { EditorState } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { useState } from '../store/context'
|
||||
import { ProseMirror } from './ProseMirror'
|
||||
import '../styles/Editor.scss'
|
||||
|
@ -9,17 +9,11 @@ export const Editor = () => {
|
|||
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
||||
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
||||
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
||||
const style = () => {
|
||||
if (store.error) {
|
||||
return `display: none;`
|
||||
} else {
|
||||
return store.markdown ? `white-space: pre-wrap;` : ''
|
||||
}
|
||||
}
|
||||
// const editorCss = (config) => css``
|
||||
const style = () => (store.error ? `display: none;` : store.markdown ? `white-space: pre-wrap;` : '')
|
||||
return (
|
||||
<ProseMirror
|
||||
// eslint-disable-next-line solid/no-react-specific-props
|
||||
className="editor"
|
||||
className='editor'
|
||||
style={style()}
|
||||
editorView={store.editorView}
|
||||
text={store.text}
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import { Switch, Match } from 'solid-js'
|
||||
import { useState } from '../store/context'
|
||||
import '../styles/Button.scss'
|
||||
import { ErrorObject, useState } from '../store/context'
|
||||
import { t } from '../../../utils/intl'
|
||||
|
||||
export default () => {
|
||||
const [store] = useState()
|
||||
return (
|
||||
<Switch fallback={<Other />}>
|
||||
<Match when={store.error.id === 'invalid_state'}>
|
||||
<InvalidState title="Invalid State" />
|
||||
<InvalidState title='Invalid State' />
|
||||
</Match>
|
||||
<Match when={store.error.id === 'invalid_config'}>
|
||||
<InvalidState title="Invalid Config" />
|
||||
<InvalidState title='Invalid Config' />
|
||||
</Match>
|
||||
<Match when={store.error.id === 'invalid_draft'}>
|
||||
<InvalidState title="Invalid Draft" />
|
||||
<Match when={store.error.id === 'invalid_file'}>
|
||||
<InvalidState title='Invalid File' />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
|
@ -25,17 +24,19 @@ const InvalidState = (props: { title: string }) => {
|
|||
const onClick = () => ctrl.clean()
|
||||
|
||||
return (
|
||||
<div class="error">
|
||||
<div class="container">
|
||||
<div class='error'>
|
||||
<div class='container'>
|
||||
<h1>{props.title}</h1>
|
||||
<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>
|
||||
<pre>
|
||||
<code>{JSON.stringify(store.error.props)}</code>
|
||||
</pre>
|
||||
<button class="primary" onClick={onClick}>
|
||||
{t('Clean')}
|
||||
<button class='primary' onClick={onClick}>
|
||||
Clean
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -47,18 +48,18 @@ const Other = () => {
|
|||
const onClick = () => ctrl.discard()
|
||||
|
||||
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 (
|
||||
<div class="error">
|
||||
<div class="container">
|
||||
<div class='error'>
|
||||
<div class='container'>
|
||||
<h1>An error occurred.</h1>
|
||||
<pre>
|
||||
<code>{getMessage()}</code>
|
||||
</pre>
|
||||
<button class="primary" onClick={onClick}>
|
||||
<button class='primary' onClick={onClick}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
import type { JSX } from 'solid-js/jsx-runtime'
|
||||
import type { Config } from '../store/context'
|
||||
import { Config } from '../store/context'
|
||||
import '../styles/Layout.scss'
|
||||
|
||||
export type Styled = {
|
||||
children: JSX.Element
|
||||
config?: Config
|
||||
'data-testid'?: string
|
||||
onClick?: () => void
|
||||
onMouseEnter?: (ev: MouseEvent) => void
|
||||
children: any;
|
||||
config?: Config;
|
||||
'data-testid'?: string;
|
||||
onClick?: () => void;
|
||||
onMouseEnter?: (e: any) => void;
|
||||
}
|
||||
|
||||
export const Layout = (props: Styled) => {
|
||||
return (
|
||||
<div onMouseEnter={props.onMouseEnter} class="layout" data-testid={props['data-testid']}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
return (<div onMouseEnter={props.onMouseEnter} class='layout' data-testid={props['data-testid']}>
|
||||
{props.children}
|
||||
</div>)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { createEffect, untrack } from 'solid-js'
|
||||
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 { Schema } from 'prosemirror-model'
|
||||
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||
import { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||
|
||||
interface Props {
|
||||
style?: string
|
||||
className?: string
|
||||
text?: Store<ProseMirrorState>
|
||||
editorView?: Store<EditorView>
|
||||
extensions?: Store<ProseMirrorExtension[]>
|
||||
onInit: (s: EditorState, v: EditorView) => void
|
||||
onReconfigure: (s: EditorState) => void
|
||||
onChange: (s: EditorState) => void
|
||||
style?: string;
|
||||
className?: string;
|
||||
text?: Store<ProseMirrorState>;
|
||||
editorView?: Store<EditorView>;
|
||||
extensions?: Store<ProseMirrorExtension[]>;
|
||||
onInit: (s: EditorState, v: EditorView) => void;
|
||||
onReconfigure: (s: EditorState) => void;
|
||||
onChange: (s: EditorState) => void;
|
||||
}
|
||||
|
||||
export const ProseMirror = (props: Props) => {
|
||||
|
@ -28,39 +28,45 @@ export const ProseMirror = (props: Props) => {
|
|||
props.onChange(newState)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
(payload: [EditorState, ProseMirrorExtension[]]) => {
|
||||
const [prevText, prevExtensions] = payload
|
||||
const text = unwrap(props.text)
|
||||
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
||||
if (!text || !extensions?.length) {
|
||||
return [text, extensions]
|
||||
}
|
||||
|
||||
if (!props.editorView) {
|
||||
const { editorState, nodeViews } = createEditorState(text, extensions)
|
||||
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
|
||||
view.focus()
|
||||
props.onInit(editorState, view)
|
||||
return [editorState, extensions]
|
||||
}
|
||||
|
||||
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
||||
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
||||
if (!editorState) return
|
||||
editorView().updateState(editorState)
|
||||
editorView().setProps({ nodeViews, dispatchTransaction })
|
||||
props.onReconfigure(editorState)
|
||||
editorView().focus()
|
||||
return [editorState, extensions]
|
||||
}
|
||||
|
||||
createEffect((payload: [EditorState, ProseMirrorExtension[]]) => {
|
||||
const [prevText, prevExtensions] = payload
|
||||
const text: EditorState = unwrap(props.text)
|
||||
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
||||
if (!text || !extensions?.length) {
|
||||
return [text, extensions]
|
||||
},
|
||||
[props.text, props.extensions]
|
||||
}
|
||||
|
||||
if (!props.editorView) {
|
||||
const { editorState, nodeViews } = createEditorState(text, extensions)
|
||||
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
|
||||
view.focus()
|
||||
props.onInit(editorState, view)
|
||||
return [editorState, extensions]
|
||||
}
|
||||
|
||||
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
||||
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
||||
if (!editorState) return
|
||||
editorView().updateState(editorState)
|
||||
editorView().setProps({ nodeViews, dispatchTransaction })
|
||||
props.onReconfigure(editorState)
|
||||
editorView().focus()
|
||||
return [editorState, extensions]
|
||||
}
|
||||
|
||||
return [text, 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 = (
|
||||
|
@ -68,8 +74,8 @@ const createEditorState = (
|
|||
extensions: ProseMirrorExtension[],
|
||||
prevText?: EditorState
|
||||
): {
|
||||
editorState: EditorState
|
||||
nodeViews: { [key: string]: NodeViewFn }
|
||||
editorState: EditorState;
|
||||
nodeViews: { [key: string]: NodeViewFn };
|
||||
} => {
|
||||
const reconfigure = text instanceof EditorState && prevText?.schema
|
||||
let schemaSpec = { nodes: {} }
|
||||
|
@ -95,10 +101,10 @@ const createEditorState = (
|
|||
|
||||
let editorState: EditorState
|
||||
if (reconfigure) {
|
||||
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
|
||||
editorState = text.reconfigure({ schema, plugins })
|
||||
} else if (text instanceof EditorState) {
|
||||
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
||||
} else if (text) {
|
||||
} else if (text){
|
||||
console.debug(text)
|
||||
editorState = EditorState.fromJSON({ schema, plugins }, 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 { 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 { isEmpty /*, isInitialized*/ } from '../prosemirror/helpers'
|
||||
import type { Styled } from './Layout'
|
||||
import { isEmpty } from '../prosemirror/helpers'
|
||||
import { Styled } from './Layout'
|
||||
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 = (
|
||||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||
) => (
|
||||
<button
|
||||
class={`sidebar-link${props.className ? ' ' + props.className : ''}`}
|
||||
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
|
||||
class={`sidebar-link${props.className ? ` ${props.className}` : ''}`}
|
||||
style={{ marginBottom: props.withMargin ? '10px' : '' }}
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
title={props.title}
|
||||
|
@ -27,59 +27,54 @@ const Link = (
|
|||
</button>
|
||||
)
|
||||
|
||||
const Keys = (props) => (
|
||||
<span>
|
||||
<For each={props.keys}>{(k: Element) => <i>{k}</i>}</For>
|
||||
</span>
|
||||
)
|
||||
|
||||
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 [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||
const toggleTheme = () => {
|
||||
document.body.classList.toggle('dark')
|
||||
ctrl.updateConfig({ theme: document.body.className })
|
||||
}
|
||||
const collabText = () => {
|
||||
if (store.collab?.started) {
|
||||
return t('Stop collab')
|
||||
} else {
|
||||
return store.collab?.error ? t('Restart collab') : t('Start collab')
|
||||
}
|
||||
}
|
||||
const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
|
||||
const editorView = () => unwrap(store.editorView)
|
||||
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 onUndo = () => undo(editorView().state, editorView().dispatch)
|
||||
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
||||
const onCopyAllAsMd = () =>
|
||||
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||
const onCopyAllAsMd = () => remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||
const onDiscard = () => ctrl.discard()
|
||||
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
||||
const toggleSidebar = () => setIsHidden(!isHidden())
|
||||
toggleSidebar()
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const DraftLink = (p: { draft: Draft }) => {
|
||||
const toggleSidebar = () => {
|
||||
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
|
||||
let content = ''
|
||||
const getContent = (node: any) => {
|
||||
if (node.text) content += node.text
|
||||
if (node.text) {
|
||||
content += node.text
|
||||
}
|
||||
|
||||
if (content.length > length) {
|
||||
content = content.slice(0, Math.max(0, length)) + '...'
|
||||
content = content.substring(0, length) + '...'
|
||||
return content
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
if (content.length >= length) break
|
||||
if (content.length >= length) {
|
||||
break
|
||||
}
|
||||
|
||||
content = getContent(child)
|
||||
}
|
||||
}
|
||||
|
@ -88,90 +83,89 @@ export const Sidebar = () => {
|
|||
}
|
||||
|
||||
const text = () =>
|
||||
p.draft.path
|
||||
? p.draft.path.slice(Math.max(0, p.draft.path.length - length))
|
||||
: getContent(p.draft.text?.doc)
|
||||
p.file.path ? p.file.path.substring(p.file.path.length - length) : getContent(p.file.text?.doc)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line solid/no-react-specific-props
|
||||
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
|
||||
{text()} {p.draft.path && '📎'}
|
||||
<Link className='file' onClick={() => onOpenFile(p.file)} data-testid='open'>
|
||||
{text()} {p.file.path && '📎'}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const onCollab = () => {
|
||||
const state = unwrap(store)
|
||||
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
||||
}
|
||||
const Keys = ({ keys }: { keys: string[] }) => (
|
||||
<span>
|
||||
{keys.map((k) => (
|
||||
<i>{k}</i>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
if (store.lastModified) setLastAction()
|
||||
})
|
||||
setLastAction(undefined)
|
||||
}, store.lastModified)
|
||||
|
||||
createEffect(() => {
|
||||
if (!lastAction()) return
|
||||
const id = setTimeout(() => {
|
||||
setLastAction()
|
||||
setLastAction(undefined)
|
||||
}, 1000)
|
||||
onCleanup(() => clearTimeout(id))
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||
<span class="sidebar-opener" onClick={toggleSidebar}>
|
||||
Советы и предложения
|
||||
</span>
|
||||
<div className={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||
<span className='sidebar-opener' onClick={toggleSidebar}>Советы и предложения</span>
|
||||
|
||||
<Off onClick={() => editorView().focus()}>
|
||||
<div class="sidebar-closer" onClick={toggleSidebar} />
|
||||
<div className='sidebar-closer' onClick={toggleSidebar}/>
|
||||
<Show when={true}>
|
||||
<div>
|
||||
{store.path && (
|
||||
<Label>
|
||||
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
||||
<i>({store.path.substring(store.path.length - 24)})</i>
|
||||
</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} />
|
||||
<label for="theme">Ночная тема</label>
|
||||
<input type='checkbox' name='theme' id='theme' onClick={toggleTheme} />
|
||||
<label for='theme'>Ночная тема</label>
|
||||
</div>
|
||||
<Link
|
||||
onClick={onDiscard}
|
||||
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
||||
data-testid="discard"
|
||||
disabled={!store.path && store.files.length === 0 && isEmpty(store.text)}
|
||||
data-testid='discard'
|
||||
>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{store.path
|
||||
? 'Close'
|
||||
: (store.drafts.length > 0 && isEmpty(store.text)
|
||||
? 'Delete ⚠️'
|
||||
: 'Clear')}{' '}
|
||||
{store.path ? 'Close' : store.files.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '}
|
||||
<Keys keys={[mod, 'w']} />
|
||||
</Link>
|
||||
<Link onClick={onUndo}>
|
||||
Undo <Keys keys={[mod, 'z']} />
|
||||
</Link>
|
||||
<Link onClick={onRedo}>
|
||||
Redo <Keys keys={[mod, ...(isMac() ? ['Shift', 'z'] : ['y'])]} />
|
||||
Redo <Keys keys={[mod, ...['Shift', 'z']]} />
|
||||
</Link>
|
||||
<Link onClick={onToggleMarkdown} data-testid="markdown">
|
||||
<Link onClick={onToggleMarkdown} data-testid='markdown'>
|
||||
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
||||
</Link>
|
||||
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
||||
<Show when={store.drafts.length > 0}>
|
||||
<Show when={store.files.length > 0}>
|
||||
<h4>Drafts:</h4>
|
||||
<p>
|
||||
<For each={store.drafts}>{(draft: Draft) => <DraftLink draft={draft} />}</For>
|
||||
<For each={store.files}>{(file) => <FileLink file={file} />}</For>
|
||||
</p>
|
||||
</Show>
|
||||
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
||||
{collabText()}
|
||||
Collab {collabText()}
|
||||
</Link>
|
||||
<Show when={collabUsers() > 0}>
|
||||
<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 { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
||||
import type { Node, Schema } from 'prosemirror-model'
|
||||
import type { EditorState } from 'prosemirror-state'
|
||||
import { Node, Schema } from 'prosemirror-model'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
|
||||
export const serialize = (state: EditorState) => {
|
||||
let text = markdownSerializer.serialize(state.doc)
|
||||
|
@ -12,24 +12,10 @@ export const serialize = (state: EditorState) => {
|
|||
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(
|
||||
{
|
||||
...defaultMarkdownSerializer.nodes,
|
||||
image(state: any, node) {
|
||||
image(state, node) {
|
||||
const alt = state.esc(node.attrs.alt || '')
|
||||
const src = node.attrs.path ?? node.attrs.src
|
||||
const title = node.attrs.title ? state.quote(node.attrs.title) : undefined
|
||||
|
@ -102,6 +88,20 @@ export const markdownSerializer = new MarkdownSerializer(
|
|||
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) => {
|
||||
if (table_child.type.name === 'table_head') serializeTableHead(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) {
|
||||
let i = idx
|
||||
function listIsTight(tokens: any, i: number) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { schema as markdownSchema } from 'prosemirror-markdown'
|
||||
import type OrderedMap from 'orderedmap'
|
||||
import { NodeSpec, Schema } from 'prosemirror-model'
|
||||
import { Schema } from 'prosemirror-model'
|
||||
import { baseKeymap } from 'prosemirror-commands'
|
||||
import { sinkListItem, liftListItem } from 'prosemirror-schema-list'
|
||||
import { history } from 'prosemirror-history'
|
||||
import { dropCursor } from 'prosemirror-dropcursor'
|
||||
import { buildKeymap } from 'prosemirror-example-setup'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const plainSchema = new Schema({
|
||||
nodes: {
|
||||
|
@ -36,16 +35,13 @@ export default (plain = false): ProseMirrorExtension => ({
|
|||
schema: () =>
|
||||
plain
|
||||
? {
|
||||
nodes: plainSchema.spec.nodes,
|
||||
marks: plainSchema.spec.marks
|
||||
}
|
||||
nodes: plainSchema.spec.nodes,
|
||||
marks: plainSchema.spec.marks
|
||||
}
|
||||
: {
|
||||
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update(
|
||||
'blockquote',
|
||||
blockquoteSchema as unknown as NodeSpec
|
||||
),
|
||||
marks: markdownSchema.spec.marks
|
||||
},
|
||||
nodes: (markdownSchema.spec.nodes as any).update('blockquote', blockquoteSchema),
|
||||
marks: markdownSchema.spec.marks
|
||||
},
|
||||
plugins: (prev, schema) => [
|
||||
...prev,
|
||||
keymap({
|
||||
|
|
|
@ -1,42 +1,42 @@
|
|||
import { inputRules } from 'prosemirror-inputrules'
|
||||
import type { Mark, MarkType } from 'prosemirror-model'
|
||||
import type { EditorState, Transaction } from 'prosemirror-state'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import { Mark, MarkType } from 'prosemirror-model'
|
||||
import { EditorState, Transaction } from 'prosemirror-state'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { keymap } from 'prosemirror-keymap'
|
||||
import { markInputRule } from './mark-input-rule'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const blank = '\u00A0'
|
||||
const blank = '\xa0'
|
||||
|
||||
const onArrow =
|
||||
(dir: 'left' | 'right') =>
|
||||
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
||||
if (!state.selection.empty) return false
|
||||
const $pos = state.selection.$head
|
||||
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
||||
const tr = state.tr
|
||||
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
||||
if (!state.selection.empty) return false
|
||||
const $pos = state.selection.$head
|
||||
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
||||
const tr = state.tr
|
||||
|
||||
if (dir === 'left') {
|
||||
const up = editorView.endOfTextblock('up')
|
||||
if (!$pos.nodeBefore && up && isCode) {
|
||||
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
||||
dispatch(tr)
|
||||
}
|
||||
} else {
|
||||
const down = editorView.endOfTextblock('down')
|
||||
if (!$pos.nodeAfter && down && isCode) {
|
||||
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
||||
dispatch(tr)
|
||||
if (dir === 'left') {
|
||||
const up = editorView.endOfTextblock('up')
|
||||
if (!$pos.nodeBefore && up && isCode) {
|
||||
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
||||
dispatch(tr)
|
||||
}
|
||||
} else {
|
||||
const down = editorView.endOfTextblock('down')
|
||||
if (!$pos.nodeAfter && down && isCode) {
|
||||
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
||||
dispatch(tr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const codeKeymap = {
|
||||
ArrowLeft: onArrow('left'),
|
||||
ArrowRight: onArrow('right')
|
||||
}
|
||||
|
||||
const codeRule = (nodeType: MarkType) => markInputRule(/`([^`]+)`$/, nodeType, null)
|
||||
const codeRule = (nodeType: MarkType) => markInputRule(/(?:`)([^`]+)(?:`)$/, nodeType)
|
||||
|
||||
export default (): ProseMirrorExtension => ({
|
||||
plugins: (prev, schema) => [
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
||||
import type { YOptions } from '../../store/context'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
import { YOptions } from '../../store/context'
|
||||
|
||||
export interface EditingProps {
|
||||
name: string
|
||||
foreground: string
|
||||
background: string
|
||||
}
|
||||
|
||||
export const cursorBuilder = (user: EditingProps): HTMLElement => {
|
||||
export const cursorBuilder = (user: any): HTMLElement => {
|
||||
const cursor = document.createElement('span')
|
||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||
cursor.setAttribute('style', `border-color: ${user.background}`)
|
||||
|
@ -23,10 +17,11 @@ export default (y: YOptions): ProseMirrorExtension => ({
|
|||
plugins: (prev) =>
|
||||
y
|
||||
? [
|
||||
...prev,
|
||||
ySyncPlugin(y.type),
|
||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||
yUndoPlugin()
|
||||
]
|
||||
...prev,
|
||||
ySyncPlugin(y.type),
|
||||
// @ts-ignore
|
||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||
yUndoPlugin()
|
||||
]
|
||||
: prev
|
||||
})
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Plugin, NodeSelection } from 'prosemirror-state'
|
||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import handleIcon from '../../../../assets/handle.svg'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
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 handle = document.createElement('span')
|
||||
|
@ -18,8 +22,8 @@ const handlePlugin = new Plugin({
|
|||
decorations(state) {
|
||||
const decos = []
|
||||
state.doc.forEach((node, pos) => {
|
||||
decos.push(Decoration.widget(pos + 1, createDragHandle))
|
||||
decos.push(
|
||||
Decoration.widget(pos + 1, createDragHandle),
|
||||
Decoration.node(pos, pos + node.nodeSize, {
|
||||
class: 'draggable'
|
||||
})
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import type { Node, NodeSpec, Schema } from 'prosemirror-model'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||
import type OrderedMap from 'orderedmap'
|
||||
import { Node, Schema } from 'prosemirror-model'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
|
||||
const REGEX = /^!\[([^[\]]*?)\]\((.+?)\)\s+/
|
||||
const MAX_MATCH = 500
|
||||
|
||||
const isUrl = (str: string) => {
|
||||
try {
|
||||
const url = new URL(str)
|
||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||
} catch {
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
||||
const isBlank = (text: string) => text === ' ' || text === '\xa0'
|
||||
|
||||
const imageInput = (schema: Schema, _path?: string) =>
|
||||
const imageInput = (schema: Schema, path?: string) =>
|
||||
new Plugin({
|
||||
props: {
|
||||
handleTextInput(view, from, to, text) {
|
||||
|
@ -30,7 +29,7 @@ const imageInput = (schema: Schema, _path?: string) =>
|
|||
Math.max(0, $from.parentOffset - MAX_MATCH),
|
||||
$from.parentOffset,
|
||||
null,
|
||||
'\uFFFC'
|
||||
'\ufffc'
|
||||
) + text
|
||||
|
||||
const match = REGEX.exec(textBefore)
|
||||
|
@ -45,6 +44,7 @@ const imageInput = (schema: Schema, _path?: string) =>
|
|||
view.dispatch(tr)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ const imageSchema = {
|
|||
src: dom.getAttribute('src'),
|
||||
title: dom.getAttribute('title'),
|
||||
alt: dom.getAttribute('alt'),
|
||||
path: (dom as NodeSpec).dataset.path
|
||||
path: dom.getAttribute('data-path')
|
||||
})
|
||||
}
|
||||
],
|
||||
|
@ -102,12 +102,12 @@ class ImageView {
|
|||
contentDOM: Element
|
||||
container: HTMLElement
|
||||
handle: HTMLElement
|
||||
onResizeFn: (e: Event) => void
|
||||
onResizeEndFn: (e: Event) => void
|
||||
onResizeFn: any
|
||||
onResizeEndFn: any
|
||||
width: number
|
||||
updating: number
|
||||
|
||||
constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, _path: string) {
|
||||
constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, path: string) {
|
||||
this.node = node
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
|
@ -162,12 +162,12 @@ class ImageView {
|
|||
export default (path?: string): ProseMirrorExtension => ({
|
||||
schema: (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)],
|
||||
nodeViews: {
|
||||
image: (node, view, getPos) => {
|
||||
return new ImageView(node, view, getPos, view.state.schema, path)
|
||||
}
|
||||
} as unknown as { [key: string]: NodeViewFn }
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { Mark, Node, Schema } from 'prosemirror-model'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { Mark, Node, Schema } from 'prosemirror-model'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
||||
const REGEX = /(^|\s)\[(.+)\]\(([^ ]+)(?: "(.+)")?\)/
|
||||
|
||||
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
||||
let markPos = { from: -1, to: -1 }
|
||||
|
@ -29,8 +29,9 @@ const markdownLinks = (schema: Schema) =>
|
|||
apply(tr, state) {
|
||||
const action = tr.getMeta(this)
|
||||
if (action?.pos) {
|
||||
(state as any).pos = action.pos
|
||||
state.pos = action.pos
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
},
|
||||
|
@ -53,12 +54,11 @@ const markdownLinks = (schema: Schema) =>
|
|||
const resolvePos = (view: EditorView, pos: number) => {
|
||||
try {
|
||||
return view.state.doc.resolve(pos)
|
||||
} catch {
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const toLink = (view: EditorView, tr: Transaction) => {
|
||||
const sel = view.state.selection
|
||||
const state = pluginKey.getState(view.state)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { InputRule } from 'prosemirror-inputrules'
|
||||
import type { EditorState } from 'prosemirror-state'
|
||||
import type { MarkType } from 'prosemirror-model'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
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) => {
|
||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||
const tr = state.tr
|
||||
|
@ -13,6 +13,7 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs) =>
|
|||
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
||||
if (node.marks.length > 0) {
|
||||
hasMarks = true
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -22,7 +23,6 @@ export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs) =>
|
|||
|
||||
if (textEnd < end) tr.delete(textEnd, end)
|
||||
if (textStart > start) tr.delete(start, textStart)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
end = start + match[1].length
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
emDash,
|
||||
ellipsis
|
||||
} from 'prosemirror-inputrules'
|
||||
import type { NodeType, Schema } from 'prosemirror-model'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { NodeType, Schema } from 'prosemirror-model'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
||||
|
||||
|
@ -16,10 +16,10 @@ const orderedListRule = (nodeType: NodeType) =>
|
|||
/^(\d+)\.\s$/,
|
||||
nodeType,
|
||||
(match) => ({ order: +match[1] }),
|
||||
(match, node) => node.childCount + node.attrs.order === +match[1]
|
||||
(match, node) => node.childCount + node.attrs.order == +match[1]
|
||||
)
|
||||
|
||||
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([*+-])\s$/, nodeType)
|
||||
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([-+*])\s$/, nodeType)
|
||||
|
||||
const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
||||
textblockTypeInputRule(new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, (match) => ({
|
||||
|
@ -27,7 +27,7 @@ const headingRule = (nodeType: NodeType, maxLevel: number) =>
|
|||
}))
|
||||
|
||||
const markdownRules = (schema: Schema) => {
|
||||
const rules = [...smartQuotes, ellipsis, emDash]
|
||||
const rules = smartQuotes.concat(ellipsis, emDash)
|
||||
if (schema.nodes.blockquote) rules.push(blockQuoteRule(schema.nodes.blockquote))
|
||||
if (schema.nodes.ordered_list) rules.push(orderedListRule(schema.nodes.ordered_list))
|
||||
if (schema.nodes.bullet_list) rules.push(bulletListRule(schema.nodes.bullet_list))
|
||||
|
|
|
@ -13,40 +13,38 @@ import {
|
|||
Dropdown
|
||||
} from 'prosemirror-menu'
|
||||
|
||||
import type { MenuItemSpec, MenuElement } from 'prosemirror-menu'
|
||||
|
||||
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 type { ProseMirrorExtension } from '../helpers'
|
||||
import type { Attrs, MarkType, NodeType, Schema } from 'prosemirror-model'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
// Helpers to create specific types of items
|
||||
|
||||
function canInsert(state: EditorState, nodeType: NodeType) {
|
||||
function canInsert(state, nodeType) {
|
||||
const $from = state.selection.$from
|
||||
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
const index = $from.index(d)
|
||||
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function insertImageItem(nodeType: NodeType) {
|
||||
function insertImageItem(nodeType) {
|
||||
return new MenuItem({
|
||||
icon: icons.image,
|
||||
label: 'image',
|
||||
enable(state) {
|
||||
return canInsert(state, nodeType)
|
||||
},
|
||||
run(state: EditorState, _, view: EditorView) {
|
||||
const { from, to, node } = state.selection as NodeSelection
|
||||
run(state, _, view) {
|
||||
const { from, to } = state.selection
|
||||
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({
|
||||
title: 'Insert image',
|
||||
|
@ -62,8 +60,7 @@ function insertImageItem(nodeType: NodeType) {
|
|||
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line no-shadow
|
||||
callback(attrs: Attrs) {
|
||||
callback(attrs) {
|
||||
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))
|
||||
view.focus()
|
||||
}
|
||||
|
@ -72,31 +69,41 @@ function insertImageItem(nodeType: NodeType) {
|
|||
})
|
||||
}
|
||||
|
||||
function cmdItem(cmd: Command, options: MenuItemSpec) {
|
||||
const passedOptions = { label: options.title, run: cmd } as MenuItemSpec
|
||||
Object.keys(options).forEach((prop) => (passedOptions[prop] = options[prop]))
|
||||
// TODO: enable/disable items logix
|
||||
passedOptions.select = (state) => cmd(state)
|
||||
return new MenuItem(passedOptions as MenuItemSpec)
|
||||
function cmdItem(cmd, options) {
|
||||
const passedOptions = {
|
||||
label: options.title,
|
||||
run: cmd
|
||||
}
|
||||
|
||||
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: EditorState, type: MarkType) {
|
||||
function markActive(state, type) {
|
||||
const { from, $from, to, empty } = state.selection
|
||||
|
||||
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
||||
|
||||
return state.doc.rangeHasMark(from, to, type)
|
||||
}
|
||||
|
||||
function markItem(markType: MarkType, options: MenuItemSpec) {
|
||||
function markItem(markType, options) {
|
||||
const passedOptions = {
|
||||
active(state) {
|
||||
return markActive(state, markType)
|
||||
}
|
||||
} as MenuItemSpec
|
||||
Object.keys(options).forEach((prop: string) => (passedOptions[prop] = options[prop]))
|
||||
},
|
||||
enable: true
|
||||
}
|
||||
|
||||
for (const prop in options) passedOptions[prop] = options[prop]
|
||||
|
||||
return cmdItem(toggleMark(markType), passedOptions)
|
||||
}
|
||||
|
||||
function linkItem(markType: MarkType) {
|
||||
function linkItem(markType) {
|
||||
return new MenuItem({
|
||||
title: 'Add or remove link',
|
||||
icon: {
|
||||
|
@ -104,21 +111,27 @@ function linkItem(markType: MarkType) {
|
|||
height: 18,
|
||||
path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z'
|
||||
},
|
||||
active: (state) => Boolean(markActive(state, markType)),
|
||||
enable: (state: EditorState) => !state.selection.empty,
|
||||
run(state: EditorState, dispatch: (t: Transaction) => void, view: EditorView) {
|
||||
active(state) {
|
||||
return markActive(state, markType)
|
||||
},
|
||||
enable(state) {
|
||||
return !state.selection.empty
|
||||
},
|
||||
run(state, dispatch, view) {
|
||||
if (markActive(state, markType)) {
|
||||
toggleMark(markType)(state, dispatch)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
openPrompt({
|
||||
fields: {
|
||||
href: new TextField({
|
||||
label: 'Link target',
|
||||
required: true
|
||||
})
|
||||
}),
|
||||
},
|
||||
callback(attrs: Attrs) {
|
||||
callback(attrs) {
|
||||
toggleMark(markType, attrs)(view.state, view.dispatch)
|
||||
view.focus()
|
||||
}
|
||||
|
@ -127,8 +140,7 @@ function linkItem(markType: MarkType) {
|
|||
})
|
||||
}
|
||||
|
||||
function wrapListItem(nodeType: NodeType, options: MenuItemSpec & { attrs: Attrs }) {
|
||||
options.run = (_) => true
|
||||
function wrapListItem(nodeType, options) {
|
||||
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
||||
}
|
||||
|
||||
|
@ -190,24 +202,9 @@ function wrapListItem(nodeType: NodeType, options: MenuItemSpec & { attrs: Attrs
|
|||
// **`fullMenu`**`: [[MenuElement]]`
|
||||
// : An array of arrays of menu elements for use as the full menu
|
||||
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
|
||||
/*
|
||||
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) {
|
||||
export function buildMenuItems(schema) {
|
||||
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
||||
let type: NodeType | MarkType
|
||||
let type
|
||||
|
||||
if ((type = schema.marks.strong)) {
|
||||
r.toggleStrong = markItem(type, {
|
||||
|
@ -215,9 +212,9 @@ export function buildMenuItems(schema: Schema) {
|
|||
icon: {
|
||||
width: 13,
|
||||
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)) {
|
||||
|
@ -226,21 +223,21 @@ export function buildMenuItems(schema: Schema) {
|
|||
icon: {
|
||||
width: 14,
|
||||
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)) {
|
||||
r.toggleCode = markItem(type, {
|
||||
title: 'Toggle code font',
|
||||
icon: icons.code
|
||||
} as MenuItemSpec)
|
||||
})
|
||||
}
|
||||
|
||||
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)) {
|
||||
r.wrapBulletList = wrapListItem(type, {
|
||||
|
@ -250,7 +247,7 @@ export function buildMenuItems(schema: Schema) {
|
|||
height: 16,
|
||||
path: 'M0.000114441 1.6C0.000114441 0.714665 0.71478 0 1.60011 0C2.48544 0 3.20011 0.714665 3.20011 1.6C3.20011 2.48533 2.48544 3.19999 1.60011 3.19999C0.71478 3.19999 0.000114441 2.48533 0.000114441 1.6ZM0 8.00013C0 7.1148 0.714665 6.40014 1.6 6.40014C2.48533 6.40014 3.19999 7.1148 3.19999 8.00013C3.19999 8.88547 2.48533 9.60013 1.6 9.60013C0.714665 9.60013 0 8.88547 0 8.00013ZM1.6 12.8C0.714665 12.8 0 13.5254 0 14.4C0 15.2747 0.725332 16 1.6 16C2.47466 16 3.19999 15.2747 3.19999 14.4C3.19999 13.5254 2.48533 12.8 1.6 12.8ZM19.7333 15.4662H4.79999V13.3329H19.7333V15.4662ZM4.79999 9.06677H19.7333V6.93344H4.79999V9.06677ZM4.79999 2.66664V0.533307H19.7333V2.66664H4.79999Z'
|
||||
}
|
||||
} as MenuItemSpec & { attrs: Attrs })
|
||||
})
|
||||
}
|
||||
|
||||
if ((type = schema.nodes.ordered_list)) {
|
||||
|
@ -261,7 +258,7 @@ export function buildMenuItems(schema: Schema) {
|
|||
height: 16,
|
||||
path: 'M2.00002 4.00003H1.00001V1.00001H0V0H2.00002V4.00003ZM2.00002 13.5V13H0V12H3.00003V16H0V15H2.00002V14.5H1.00001V13.5H2.00002ZM0 6.99998H1.80002L0 9.1V10H3.00003V9H1.20001L3.00003 6.89998V5.99998H0V6.99998ZM4.9987 2.99967V0.999648H18.9988V2.99967H4.9987ZM4.9987 15.0001H18.9988V13.0001H4.9987V15.0001ZM18.9988 8.99987H4.9987V6.99986H18.9988V8.99987Z'
|
||||
}
|
||||
} as MenuItemSpec & { attrs: Attrs })
|
||||
})
|
||||
}
|
||||
|
||||
if ((type = schema.nodes.blockquote)) {
|
||||
|
@ -312,36 +309,28 @@ export function buildMenuItems(schema: Schema) {
|
|||
r.insertHorizontalRule = new MenuItem({
|
||||
label: '---',
|
||||
icon: icons.horizontal_rule,
|
||||
enable: (state) => canInsert(state, hr),
|
||||
run(state: EditorState, dispatch: (tr: Transaction) => void) {
|
||||
enable(state) {
|
||||
return canInsert(state, hr)
|
||||
},
|
||||
run(state, dispatch) {
|
||||
dispatch(state.tr.replaceSelectionWith(hr.create()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const tMenu = new Dropdown(
|
||||
[
|
||||
r.makeHead1 as MenuElement,
|
||||
r.makeHead2 as MenuElement,
|
||||
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,
|
||||
height: 12,
|
||||
path: 'M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z'
|
||||
}
|
||||
}
|
||||
)
|
||||
r.listMenu = [r.wrapBulletList as MenuItem, r.wrapOrderedList as MenuItem]
|
||||
r.inlineMenu = [r.toggleStrong as MenuItem, r.toggleEm as MenuItem, r.toggleMark as MenuItem]
|
||||
r.fullMenu = [...r.inlineMenu, tMenu as MenuItem, ...r.listMenu].filter(Boolean)
|
||||
const cut = (arr) => arr.filter((x) => x)
|
||||
r.typeMenu = new Dropdown(
|
||||
cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]),
|
||||
{ label: 'Тт', icon: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
||||
} })
|
||||
// r.blockMenu = []
|
||||
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
|
||||
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
||||
r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
|
@ -349,8 +338,8 @@ export default (): ProseMirrorExtension => ({
|
|||
plugins: (prev, schema) => [
|
||||
...prev,
|
||||
menuBar({
|
||||
floating: true,
|
||||
content: buildMenuItems(schema).fullMenu as any // NOTE: MenuItem and MenuElement are compatible
|
||||
floating: false,
|
||||
content: buildMenuItems(schema).fullMenu
|
||||
})
|
||||
]
|
||||
})
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
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 nodes = []
|
||||
fragment.forEach((child: Node) => {
|
||||
if (child.isText) {
|
||||
let pos = 0
|
||||
let match
|
||||
let match: any
|
||||
|
||||
while ((match = URL_REGEX.exec(child.text)) !== null) {
|
||||
const start = match.index
|
||||
|
@ -64,7 +64,7 @@ const pasteMarkdown = (schema: Schema) => {
|
|||
event.preventDefault()
|
||||
|
||||
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 tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd))
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||
import { isEmpty, ProseMirrorExtension } from '../helpers'
|
||||
import { ProseMirrorExtension, isEmpty } from '../helpers'
|
||||
|
||||
const placeholder = (text: string) =>
|
||||
new Plugin({
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
const prefix = 'ProseMirror-prompt'
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function openPrompt(options) {
|
||||
export function openPrompt(options: any) {
|
||||
const wrapper = document.body.appendChild(document.createElement('div'))
|
||||
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)
|
||||
const close = () => {
|
||||
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) => {
|
||||
domFields.push(options.fields[name].render())
|
||||
})
|
||||
|
@ -32,7 +32,7 @@ export function openPrompt(options) {
|
|||
if (options.title) {
|
||||
form.appendChild(document.createElement('h5')).textContent = options.title
|
||||
}
|
||||
domFields.forEach((field) => {
|
||||
domFields.forEach((field: any) => {
|
||||
form.appendChild(document.createElement('div')).appendChild(field)
|
||||
})
|
||||
const buttons = form.appendChild(document.createElement('div'))
|
||||
|
@ -59,25 +59,24 @@ export function openPrompt(options) {
|
|||
})
|
||||
|
||||
form.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (e.keyCode == 27) {
|
||||
e.preventDefault()
|
||||
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()
|
||||
submit()
|
||||
} else if (e.key === 'Tab') {
|
||||
} else if (e.keyCode == 9) {
|
||||
window.setTimeout(() => {
|
||||
if (!wrapper.contains(document.activeElement)) close()
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
const inpel = form.elements[0] as HTMLInputElement
|
||||
if (inpel) inpel.focus()
|
||||
const input: any = form.elements[0]
|
||||
if (input) input.focus()
|
||||
}
|
||||
|
||||
function getValues(fields, domFields) {
|
||||
function getValues(fields: any, domFields: any) {
|
||||
const result = Object.create(null)
|
||||
let i = 0
|
||||
fields.forEarch((name) => {
|
||||
|
@ -94,35 +93,23 @@ function getValues(fields, domFields) {
|
|||
return result
|
||||
}
|
||||
|
||||
function reportInvalid(dom: HTMLElement, message: string) {
|
||||
function reportInvalid(dom: any, message: any) {
|
||||
const parent = dom.parentNode
|
||||
const msg = parent.appendChild(document.createElement('div'))
|
||||
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
||||
msg.style.top = dom.offsetTop - 5 + 'px'
|
||||
msg.className = 'ProseMirror-invalid'
|
||||
msg.textContent = message
|
||||
// eslint-disable-next-line unicorn/prefer-dom-node-remove
|
||||
setTimeout(() => parent.removeChild(msg), 1500)
|
||||
}
|
||||
|
||||
interface FieldOptions {
|
||||
options: { value: string; label: string }[]
|
||||
required: boolean
|
||||
label: string
|
||||
value: string
|
||||
validateType: (v) => boolean
|
||||
validate: (v) => boolean
|
||||
read: (v) => string
|
||||
clean: (v) => boolean
|
||||
}
|
||||
|
||||
export class Field {
|
||||
options: FieldOptions
|
||||
constructor(options) {
|
||||
options: any
|
||||
constructor(options: any) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
read(dom) {
|
||||
read(dom: any) {
|
||||
return dom.value
|
||||
}
|
||||
// :: (any) → ?string
|
||||
|
@ -131,12 +118,13 @@ export class Field {
|
|||
return typeof _value === typeof ''
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
validate(value: any) {
|
||||
if (!value && this.options.required) return 'Required field'
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -156,10 +144,10 @@ export class TextField extends Field {
|
|||
export class SelectField extends Field {
|
||||
render() {
|
||||
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'))
|
||||
opt.value = o.value
|
||||
opt.selected = o.value === this.options.value
|
||||
opt.selected = o.value == this.options.value
|
||||
opt.label = o.label
|
||||
})
|
||||
return select
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Plugin } from 'prosemirror-state'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { EditorView } from 'prosemirror-view'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const scroll = (view: EditorView) => {
|
||||
if (!view.state.selection.empty) return false
|
||||
|
|
|
@ -1,57 +1,60 @@
|
|||
import { renderGrouped } from 'prosemirror-menu'
|
||||
import { EditorState, Plugin } from 'prosemirror-state'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { buildMenuItems } from './menu'
|
||||
import { renderGrouped } from "prosemirror-menu";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { ProseMirrorExtension } from "../helpers";
|
||||
import { buildMenuItems } from "./menu";
|
||||
|
||||
export class SelectionTooltip {
|
||||
tooltip: HTMLElement
|
||||
tooltip: any;
|
||||
|
||||
constructor(view: EditorView, schema) {
|
||||
this.tooltip = document.createElement('div')
|
||||
this.tooltip.className = 'tooltip'
|
||||
view.dom.parentNode.appendChild(this.tooltip)
|
||||
console.debug('[prosemirror] selection view', view)
|
||||
console.debug('[prosemirror] selection menu', buildMenuItems(schema).fullMenu)
|
||||
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
||||
this.tooltip.appendChild(dom)
|
||||
this.update(view, null)
|
||||
constructor(view: any, schema: any) {
|
||||
this.tooltip = document.createElement("div");
|
||||
this.tooltip.className = "tooltip";
|
||||
view.dom.parentNode.appendChild(this.tooltip);
|
||||
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu);
|
||||
this.tooltip.appendChild(dom);
|
||||
this.update(view, null);
|
||||
}
|
||||
|
||||
update(view: EditorView, lastState: EditorState) {
|
||||
const state = view.state
|
||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||
return
|
||||
}
|
||||
update(view: any, lastState: any) {
|
||||
const state = view.state;
|
||||
if (
|
||||
lastState &&
|
||||
lastState.doc.eq(state.doc) &&
|
||||
lastState.selection.eq(state.selection)
|
||||
)
|
||||
{return;}
|
||||
|
||||
if (state.selection.empty) {
|
||||
this.tooltip.style.display = 'none'
|
||||
return
|
||||
this.tooltip.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.tooltip.style.display = ''
|
||||
const { from, to } = state.selection
|
||||
this.tooltip.style.display = "";
|
||||
const { from, to } = state.selection;
|
||||
const start = view.coordsAtPos(from),
|
||||
end = view.coordsAtPos(to)
|
||||
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||
this.tooltip.style.left = left - box.left + 'px'
|
||||
this.tooltip.style.bottom = box.bottom - (start.top + 15) + 'px'
|
||||
end = view.coordsAtPos(to);
|
||||
const box = this.tooltip.offsetParent.getBoundingClientRect();
|
||||
const left = Math.max((start.left + end.left) / 2, start.left + 3);
|
||||
this.tooltip.style.left = left - box.left + "px";
|
||||
this.tooltip.style.bottom = box.bottom - (start.top + 15) + "px";
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.tooltip.remove()
|
||||
this.tooltip.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function toolTip(schema) {
|
||||
export function toolTip(schema: any) {
|
||||
return new Plugin({
|
||||
view(editorView: EditorView) {
|
||||
return new SelectionTooltip(editorView, schema)
|
||||
}
|
||||
})
|
||||
view(editorView: any) {
|
||||
return new SelectionTooltip(editorView, schema);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 type { MarkType } from 'prosemirror-model'
|
||||
import { MarkType } from 'prosemirror-model'
|
||||
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 = {
|
||||
strikethrough: {
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
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 { keymap } from 'prosemirror-keymap'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import type OrderedMap from 'orderedmap'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
export const tableInputRule = (schema: Schema) =>
|
||||
new InputRule(
|
||||
new RegExp('^\\|{2,}\\s$'),
|
||||
(state: EditorState, match: string[], start: number, end: number) => {
|
||||
const tr = state.tr
|
||||
const columns = [...Array.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 cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
||||
const table = schema.node(schema.nodes.table, {}, [
|
||||
|
@ -175,9 +174,8 @@ const getTextSize = (n: Node) => {
|
|||
export default (): ProseMirrorExtension => ({
|
||||
schema: (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) => [
|
||||
keymap({
|
||||
'Ctrl-Enter': (state, dispatch) => {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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 { keymap } from 'prosemirror-keymap'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import type { ProseMirrorExtension } from '../helpers'
|
||||
import { inputRules } from 'prosemirror-inputrules'
|
||||
import { ProseMirrorExtension } from '../helpers'
|
||||
|
||||
const todoListRule = (nodeType: NodeType) =>
|
||||
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
||||
|
@ -59,9 +60,7 @@ class TodoItemView {
|
|||
this.contentDOM = res.contentDOM
|
||||
this.view = view
|
||||
this.getPos = getPos
|
||||
;(this.dom as Element)
|
||||
.querySelector('input')
|
||||
.addEventListener('click', () => this.handleClick.bind(this))
|
||||
;(this.dom as Element).querySelector('input').onclick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
handleClick(e: MouseEvent) {
|
||||
|
@ -88,8 +87,8 @@ export default (): ProseMirrorExtension => ({
|
|||
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
||||
],
|
||||
nodeViews: {
|
||||
todo_item: (node: any, view, getPos) => {
|
||||
todo_item: (node, view, getPos) => {
|
||||
return new TodoItemView(node, view, getPos)
|
||||
}
|
||||
} as any
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Plugin, EditorState } from 'prosemirror-state'
|
||||
import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
||||
import { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||
import { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
||||
|
||||
export interface ProseMirrorExtension {
|
||||
schema?: (prev: SchemaSpec) => SchemaSpec
|
||||
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
|
||||
nodeViews?: { [key: string]: NodeViewFn }
|
||||
schema?: (prev: SchemaSpec) => SchemaSpec;
|
||||
plugins?: (prev: Plugin[], schema: Schema) => Plugin[];
|
||||
nodeViews?: { [key: string]: NodeViewFn };
|
||||
}
|
||||
|
||||
export type ProseMirrorState = EditorState | unknown
|
||||
|
@ -21,7 +21,7 @@ export const isInitialized = (state: any) => state !== undefined && state instan
|
|||
|
||||
export const isEmpty = (state: any) =>
|
||||
!isInitialized(state) ||
|
||||
(state.doc.childCount === 1 &&
|
||||
(state.doc.childCount == 1 &&
|
||||
!state.doc.firstChild.type.spec.code &&
|
||||
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 type { ProseMirrorExtension } from './helpers'
|
||||
import { ProseMirrorExtension } from './helpers'
|
||||
import { Schema } from 'prosemirror-model'
|
||||
import { t } from '../../../utils/intl'
|
||||
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 link from './extension/link'
|
||||
// import scroll from './prosemirror/extension/scroll'
|
||||
import todoList from './extension/todo-list'
|
||||
import code from './extension/code'
|
||||
import strikethrough from './extension/strikethrough'
|
||||
import placeholder from './extension/placeholder'
|
||||
// import menu from './extension/menu'
|
||||
import image from './extension/image'
|
||||
import dragHandle from './extension/drag-handle'
|
||||
import pasteMarkdown from './extension/paste-markdown'
|
||||
import table from './extension/table'
|
||||
import collab from './extension/collab'
|
||||
import type { Config, YOptions } from '../store/context'
|
||||
import { Config, YOptions } from '../store/context'
|
||||
import selectionMenu from './extension/selection'
|
||||
import type { Command } from 'prosemirror-state'
|
||||
import placeholder from './extension/placeholder'
|
||||
import todoList from './extension/todo-list'
|
||||
import strikethrough from './extension/strikethrough'
|
||||
import scrollPlugin from './extension/scroll'
|
||||
|
||||
interface ExtensionsProps {
|
||||
data?: unknown
|
||||
keymap?: { [key: string]: Command }
|
||||
config: Config
|
||||
markdown: boolean
|
||||
path?: string
|
||||
y?: YOptions
|
||||
schema?: Schema
|
||||
collab?: boolean
|
||||
typewriterMode?: boolean
|
||||
interface Props {
|
||||
data?: unknown;
|
||||
keymap?: any;
|
||||
config: Config;
|
||||
markdown: boolean;
|
||||
path?: string;
|
||||
y?: YOptions;
|
||||
schema?: Schema;
|
||||
}
|
||||
|
||||
const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({
|
||||
const customKeymap = (props: Props): ProseMirrorExtension => ({
|
||||
plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev)
|
||||
})
|
||||
/*
|
||||
const codeMirrorKeymap = (props: Props) => {
|
||||
const keys = []
|
||||
for (const key in props.keymap) {
|
||||
keys.push({key: key, run: props.keymap[key]})
|
||||
}
|
||||
|
||||
export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => {
|
||||
const eee = [
|
||||
placeholder(t('Just start typing...')),
|
||||
customKeymap(props),
|
||||
base(props.markdown),
|
||||
selectionMenu(),
|
||||
scrollPlugin(props.config?.typewriterMode)
|
||||
]
|
||||
if (props.markdown) {
|
||||
eee.push(
|
||||
return cmKeymap.of(keys)
|
||||
}
|
||||
*/
|
||||
export const createExtensions = (props: Props): ProseMirrorExtension[] =>
|
||||
props.markdown
|
||||
? [
|
||||
placeholder('Просто начните...'),
|
||||
customKeymap(props),
|
||||
base(props.markdown),
|
||||
collab(props.y),
|
||||
selectionMenu()
|
||||
]
|
||||
: [
|
||||
selectionMenu(),
|
||||
customKeymap(props),
|
||||
base(props.markdown),
|
||||
markdown(),
|
||||
todoList(),
|
||||
dragHandle(),
|
||||
|
@ -55,21 +62,19 @@ export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[]
|
|||
link(),
|
||||
table(),
|
||||
image(props.path),
|
||||
pasteMarkdown()
|
||||
pasteMarkdown(),
|
||||
collab(props.y)
|
||||
// scroll(props.config.typewriterMode),
|
||||
/*
|
||||
codeBlock({
|
||||
theme: codeTheme(props.config),
|
||||
typewriterMode: props.config.typewriterMode,
|
||||
fontSize: props.config.fontSize,
|
||||
prettier: props.config.prettier,
|
||||
extensions: () => [codeMirrorKeymap(props)],
|
||||
}),
|
||||
*/
|
||||
)
|
||||
}
|
||||
if (props.collab) eee.push(collab(props.y))
|
||||
return eee
|
||||
}
|
||||
codeBlock({
|
||||
theme: codeTheme(props.config),
|
||||
typewriterMode: props.config.typewriterMode,
|
||||
fontSize: props.config.fontSize,
|
||||
prettier: props.config.prettier,
|
||||
extensions: () => [codeMirrorKeymap(props)],
|
||||
}),
|
||||
*/
|
||||
]
|
||||
|
||||
export const createEmptyText = () => ({
|
||||
doc: {
|
||||
|
@ -83,7 +88,7 @@ export const createEmptyText = () => ({
|
|||
}
|
||||
})
|
||||
|
||||
export const createSchema = (props: ExtensionsProps) => {
|
||||
export const createSchema = (props: Props) => {
|
||||
const extensions = createExtensions({
|
||||
config: props.config,
|
||||
markdown: props.markdown,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { EditorState } from 'prosemirror-state'
|
||||
import { EditorState } from 'prosemirror-state'
|
||||
import { serialize } from './markdown'
|
||||
|
||||
export const copy = async (text: string): Promise<void> => {
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
import { Store, createStore, unwrap } from 'solid-js/store'
|
||||
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 { selectAll, deleteSelection } from 'prosemirror-commands'
|
||||
import * as Y from 'yjs'
|
||||
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 { 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 db from '../db'
|
||||
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 isDraft = (x): boolean => x && (x.text || x.path)
|
||||
const mod = 'Ctrl'
|
||||
const isText = (x: any) => x && x.doc && x.selection
|
||||
const isState = (x: any) => typeof x.lastModified !== 'string' && Array.isArray(x.files)
|
||||
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 onNew = () => {
|
||||
newDraft()
|
||||
return true
|
||||
}
|
||||
|
||||
const onDiscard = () => {
|
||||
discard()
|
||||
return true
|
||||
|
@ -32,14 +31,19 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
const onToggleMarkdown = () => toggleMarkdown()
|
||||
|
||||
const onUndo = () => {
|
||||
if (!isInitialized(store.text as EditorState)) return
|
||||
if (!isInitialized(store.text)) return
|
||||
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
|
||||
}
|
||||
|
||||
const onRedo = () => {
|
||||
if (!isInitialized(store.text as EditorState)) return
|
||||
if (!isInitialized(store.text)) return
|
||||
const text = store.text as EditorState
|
||||
if (store.collab?.started) {
|
||||
yRedo(text)
|
||||
|
@ -51,59 +55,53 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
}
|
||||
|
||||
const keymap = {
|
||||
[`${mod}-n`]: onNew,
|
||||
[`${mod}-w`]: onDiscard,
|
||||
[`${mod}-z`]: onUndo,
|
||||
[`Shift-${mod}-z`]: onRedo,
|
||||
[`${mod}-y`]: onRedo,
|
||||
[`${mod}-m`]: onToggleMarkdown
|
||||
} as { [key: string]: Command }
|
||||
}
|
||||
|
||||
const createTextFromDraft = async (d: Draft): Promise<Draft> => {
|
||||
let draft = d
|
||||
const createTextFromFile = async (file: File) => {
|
||||
const state = unwrap(store)
|
||||
if (draft.path) {
|
||||
draft = await loadDraft(state.config, draft.path)
|
||||
}
|
||||
|
||||
const extensions = createExtensions({
|
||||
config: state.config,
|
||||
markdown: draft.markdown,
|
||||
path: draft.path,
|
||||
markdown: file.markdown,
|
||||
path: file.path,
|
||||
keymap
|
||||
})
|
||||
|
||||
return {
|
||||
text: draft.text,
|
||||
text: file.text,
|
||||
extensions,
|
||||
lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined,
|
||||
path: draft.path,
|
||||
markdown: draft.markdown
|
||||
lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
|
||||
path: file.path,
|
||||
markdown: file.markdown
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
const addToDrafts = (drafts: Draft[], prev: Draft) => {
|
||||
const text = prev.path ? undefined : JSON.stringify(prev.text)
|
||||
const addToFiles = (files: File[], prev: State) => {
|
||||
const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
|
||||
return [
|
||||
...drafts,
|
||||
...files,
|
||||
{
|
||||
body: text,
|
||||
lastModified: prev.lastModified,
|
||||
text,
|
||||
lastModified: prev.lastModified?.toISOString(),
|
||||
path: prev.path,
|
||||
markdown: prev.markdown
|
||||
} as Draft
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const discardText = async () => {
|
||||
const state = unwrap(store)
|
||||
const index = state.drafts.length - 1
|
||||
const draft = index !== -1 ? state.drafts[index] : undefined
|
||||
const index = state.files.length - 1
|
||||
const file = index !== -1 ? state.files[index] : undefined
|
||||
|
||||
let next
|
||||
if (draft) {
|
||||
next = await createTextFromDraft(draft)
|
||||
let next: Partial<State>
|
||||
if (file) {
|
||||
next = await createTextFromFile(file)
|
||||
} else {
|
||||
const extensions = createExtensions({
|
||||
config: state.config ?? store.config,
|
||||
|
@ -114,73 +112,89 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
next = {
|
||||
text: createEmptyText(),
|
||||
extensions,
|
||||
lastModified: new Date(),
|
||||
lastModified: undefined,
|
||||
path: undefined,
|
||||
markdown: state.markdown
|
||||
}
|
||||
}
|
||||
|
||||
const drafts = state.drafts.filter((f: Draft) => f !== draft)
|
||||
const files = state.files.filter((f: File) => f !== file)
|
||||
|
||||
setState({
|
||||
drafts,
|
||||
files,
|
||||
...next,
|
||||
collab: state.collab,
|
||||
collab: file ? undefined : state.collab,
|
||||
error: undefined
|
||||
})
|
||||
}
|
||||
|
||||
const readStoredState = async (): Promise<State> => {
|
||||
const fetchData = async (): Promise<State> => {
|
||||
const state: State = unwrap(store)
|
||||
const room = window.location.pathname?.slice(1).trim()
|
||||
const args = { draft: room }
|
||||
const args = { room: room ? room : undefined }
|
||||
const data = await db.get('state')
|
||||
let parsed: any
|
||||
if (data !== undefined) {
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
let text = state.text
|
||||
if (parsed.text) {
|
||||
if (!isText(parsed.text)) {
|
||||
throw new ServiceError('invalid_state', parsed.text)
|
||||
}
|
||||
text = parsed.text
|
||||
}
|
||||
|
||||
const extensions = createExtensions({
|
||||
path: parsed.path,
|
||||
markdown: parsed.markdown,
|
||||
keymap,
|
||||
config: undefined
|
||||
})
|
||||
|
||||
for (const draft of parsed.drafts || []) {
|
||||
if (!isDraft(draft)) {
|
||||
console.error('[editor] invalid draft', draft)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...parsed,
|
||||
text,
|
||||
extensions,
|
||||
// config,
|
||||
args,
|
||||
lastModified: new Date(parsed.lastModified)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { ...state, args }
|
||||
parsed = JSON.parse(data)
|
||||
} catch (err) {
|
||||
throw new ServiceError('invalid_state', data)
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed) {
|
||||
return { ...state, args }
|
||||
}
|
||||
|
||||
let text = state.text
|
||||
if (parsed.text) {
|
||||
if (!isText(parsed.text)) {
|
||||
throw new ServiceError('invalid_state', parsed.text)
|
||||
}
|
||||
|
||||
text = parsed.text
|
||||
}
|
||||
|
||||
const extensions = createExtensions({
|
||||
path: parsed.path,
|
||||
markdown: parsed.markdown,
|
||||
keymap,
|
||||
config: undefined
|
||||
})
|
||||
|
||||
const newState = {
|
||||
...parsed,
|
||||
text,
|
||||
extensions,
|
||||
// config,
|
||||
args
|
||||
}
|
||||
|
||||
if (newState.lastModified) {
|
||||
newState.lastModified = new Date(newState.lastModified)
|
||||
}
|
||||
|
||||
for (const file of parsed.files) {
|
||||
if (!isFile(file)) {
|
||||
throw new ServiceError('invalid_file', file)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isState(newState)) {
|
||||
throw new ServiceError('invalid_state', newState)
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
const getTheme = (state: State) => ({ theme: state.config?.theme || '' })
|
||||
const getTheme = (state: State) => ({ theme: state.config.theme })
|
||||
|
||||
const clean = () => {
|
||||
setState({
|
||||
...newState(),
|
||||
loading: 'initialized',
|
||||
drafts: [],
|
||||
files: [],
|
||||
fullscreen: store.fullscreen,
|
||||
lastModified: new Date(),
|
||||
error: undefined,
|
||||
text: undefined
|
||||
|
@ -190,7 +204,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
const discard = async () => {
|
||||
if (store.path) {
|
||||
await discardText()
|
||||
} else if (store.drafts.length > 0 && isEmpty(store.text as EditorState)) {
|
||||
} else if (store.files.length > 0 && isEmpty(store.text)) {
|
||||
await discardText()
|
||||
} else {
|
||||
selectAll(store.editorView.state, store.editorView.dispatch)
|
||||
|
@ -199,203 +213,98 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
}
|
||||
|
||||
const init = async () => {
|
||||
let state = await readStoredState()
|
||||
console.log('[editor] init with state', state)
|
||||
let data = await fetchData()
|
||||
try {
|
||||
if (state.args?.room) {
|
||||
state = await doStartCollab(state)
|
||||
} else if (state.args.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) {
|
||||
if (data.args.room) {
|
||||
data = await doStartCollab(data)
|
||||
} else if (!data.text) {
|
||||
const text = createEmptyText()
|
||||
const extensions = createExtensions({
|
||||
config: state.config ?? store.config,
|
||||
markdown: state.markdown ?? store.markdown,
|
||||
config: data.config ?? store.config,
|
||||
markdown: data.markdown ?? store.markdown,
|
||||
keymap: keymap
|
||||
})
|
||||
state = { ...state, text, extensions }
|
||||
data = { ...data, text, extensions }
|
||||
}
|
||||
} catch (error) {
|
||||
state = { ...state, error: error.errorObject }
|
||||
data = { ...data, error: error.errorObject }
|
||||
}
|
||||
|
||||
setState({
|
||||
...state,
|
||||
config: { ...state.config, ...getTheme(state) },
|
||||
...data,
|
||||
config: { ...data.config, ...getTheme(data) },
|
||||
loading: 'initialized'
|
||||
})
|
||||
console.log('[editor] initialized successfully', state)
|
||||
}
|
||||
|
||||
const loadDraft = async (config: Config, path: string): Promise<Draft> => {
|
||||
const [draft, setDraft] = createSignal<Draft>()
|
||||
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 saveState = () => debounce(async (state: State) => {
|
||||
const data: any = {
|
||||
lastModified: state.lastModified,
|
||||
files: state.files,
|
||||
config: state.config,
|
||||
path: state.path,
|
||||
markdown: state.markdown,
|
||||
collab: {
|
||||
room: state.collab?.room
|
||||
}
|
||||
}
|
||||
|
||||
const extensions = createExtensions({
|
||||
config: state.config ?? store.config,
|
||||
markdown: state.markdown ?? store.markdown,
|
||||
keymap
|
||||
})
|
||||
if (isInitialized(state.text)) {
|
||||
data.text = store.editorView.state.toJSON()
|
||||
} else if (state.text) {
|
||||
data.text = state.text
|
||||
}
|
||||
|
||||
setState({
|
||||
text: createEmptyText(),
|
||||
extensions,
|
||||
drafts,
|
||||
lastModified: undefined,
|
||||
path: undefined,
|
||||
error: undefined,
|
||||
collab: undefined
|
||||
})
|
||||
db.set('state', JSON.stringify(data))
|
||||
}, 200)
|
||||
|
||||
const setFullscreen = (fullscreen: boolean) => {
|
||||
setState({ fullscreen })
|
||||
}
|
||||
|
||||
const openDraft = async (draft: Draft) => {
|
||||
const startCollab = async () => {
|
||||
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,
|
||||
drafts: state.drafts,
|
||||
config: state.config,
|
||||
path: state.path,
|
||||
markdown: state.markdown,
|
||||
collab: {
|
||||
room: state.collab?.room
|
||||
}
|
||||
}
|
||||
|
||||
if (isInitialized(state.text as EditorState)) {
|
||||
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()
|
||||
}
|
||||
} else if (state.text) {
|
||||
data.text = state.text
|
||||
}
|
||||
|
||||
db.set('state', JSON.stringify(data))
|
||||
}, 200)
|
||||
|
||||
const startCollab = () => {
|
||||
const state: State = unwrap(store)
|
||||
const update = doStartCollab(state)
|
||||
const update = await doStartCollab(state)
|
||||
setState(update)
|
||||
}
|
||||
|
||||
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()
|
||||
state.args = { ...state.args, room }
|
||||
let newst = state
|
||||
try {
|
||||
const { roomConnect } = await import('../prosemirror/p2p')
|
||||
const [type, provider] = roomConnect(room)
|
||||
window.history.replaceState(null, '', `/${room}`)
|
||||
|
||||
const extensions = createExtensions({
|
||||
config: state.config,
|
||||
markdown: state.markdown,
|
||||
path: state.path,
|
||||
keymap,
|
||||
y: { type, provider },
|
||||
collab: true
|
||||
})
|
||||
const { roomConnect } = await import('../prosemirror/p2p')
|
||||
const [type, provider] = roomConnect(room)
|
||||
|
||||
if ((restoredRoom && !isEmpty(state.text as EditorState)) || state.path) {
|
||||
let drafts = state.drafts
|
||||
if (!state.error) {
|
||||
drafts = addToDrafts(drafts, { lastModified: new Date(), text: state.text } as Draft)
|
||||
}
|
||||
const extensions = createExtensions({
|
||||
config: state.config,
|
||||
markdown: state.markdown,
|
||||
path: state.path,
|
||||
keymap,
|
||||
y: { type, provider }
|
||||
})
|
||||
|
||||
newst = {
|
||||
...state,
|
||||
drafts,
|
||||
lastModified: undefined,
|
||||
path: undefined,
|
||||
error: undefined
|
||||
}
|
||||
window.history.replaceState(null, '', `/${room}`)
|
||||
let newState = state
|
||||
if ((backup && !isEmpty(state.text)) || state.path) {
|
||||
let files = state.files
|
||||
if (!state.error) {
|
||||
files = addToFiles(files, state)
|
||||
}
|
||||
|
||||
return {
|
||||
...newst,
|
||||
extensions,
|
||||
collab: { started: true, room, y: { type, provider } }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return {
|
||||
newState = {
|
||||
...state,
|
||||
collab: { error }
|
||||
files,
|
||||
lastModified: undefined,
|
||||
path: undefined,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...newState,
|
||||
extensions,
|
||||
collab: { started: true, room, y: { type, provider } }
|
||||
}
|
||||
}
|
||||
|
||||
const stopCollab = (state: State) => {
|
||||
|
@ -404,8 +313,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
config: state.config,
|
||||
markdown: state.markdown,
|
||||
path: state.path,
|
||||
keymap,
|
||||
collab: false
|
||||
keymap
|
||||
})
|
||||
|
||||
setState({ collab: undefined, extensions })
|
||||
|
@ -417,7 +325,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
const editorState = store.text as EditorState
|
||||
const markdown = !state.markdown
|
||||
const selection = { type: 'text', anchor: 1, head: 1 }
|
||||
let doc
|
||||
let doc: any
|
||||
|
||||
if (markdown) {
|
||||
const lines = serialize(editorState).split('\n')
|
||||
|
@ -457,7 +365,6 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
extensions,
|
||||
markdown
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
const updateConfig = (config: Partial<Config>) => {
|
||||
|
@ -482,7 +389,7 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
}
|
||||
|
||||
const updateTheme = () => {
|
||||
const { theme } = getTheme(unwrap(store))
|
||||
const { theme } = getTheme(unwrap(store))
|
||||
setState('config', { theme })
|
||||
}
|
||||
|
||||
|
@ -491,10 +398,8 @@ export const createCtrl = (initial): [Store<State>, { [key: string]: any }] => {
|
|||
discard,
|
||||
getTheme,
|
||||
init,
|
||||
loadDraft,
|
||||
newDraft,
|
||||
openDraft,
|
||||
saveState,
|
||||
setFullscreen,
|
||||
setState,
|
||||
startCollab,
|
||||
stopCollab,
|
||||
|
|
|
@ -1,84 +1,79 @@
|
|||
import { createContext, useContext } from 'solid-js'
|
||||
import type { Store } from 'solid-js/store'
|
||||
import type { XmlFragment } from 'yjs'
|
||||
import type { WebrtcProvider } from 'y-webrtc'
|
||||
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||
import type { EditorView } from 'prosemirror-view'
|
||||
import { Store } from 'solid-js/store'
|
||||
import { XmlFragment } from 'yjs'
|
||||
import { WebrtcProvider } from 'y-webrtc'
|
||||
import { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||
|
||||
export interface Args {
|
||||
cwd?: string
|
||||
draft?: string
|
||||
room?: string
|
||||
text?: string
|
||||
cwd?: string;
|
||||
file?: string;
|
||||
room?: string;
|
||||
text?: any;
|
||||
}
|
||||
|
||||
export interface PrettierConfig {
|
||||
printWidth: number
|
||||
tabWidth: number
|
||||
useTabs: boolean
|
||||
semi: boolean
|
||||
singleQuote: boolean
|
||||
printWidth: number;
|
||||
tabWidth: number;
|
||||
useTabs: boolean;
|
||||
semi: boolean;
|
||||
singleQuote: boolean;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
theme: string
|
||||
theme: string;
|
||||
// codeTheme: string;
|
||||
// alwaysOnTop: boolean;
|
||||
font: string
|
||||
fontSize: number
|
||||
contentWidth: number
|
||||
typewriterMode?: boolean;
|
||||
prettier: PrettierConfig
|
||||
font: string;
|
||||
fontSize: number;
|
||||
contentWidth: number;
|
||||
typewriterMode: boolean;
|
||||
prettier: PrettierConfig;
|
||||
}
|
||||
|
||||
export interface ErrorObject {
|
||||
id: string
|
||||
props?: any
|
||||
id: string;
|
||||
props?: unknown;
|
||||
}
|
||||
|
||||
export interface YOptions {
|
||||
type: XmlFragment
|
||||
provider: WebrtcProvider
|
||||
type: XmlFragment;
|
||||
provider: WebrtcProvider;
|
||||
}
|
||||
|
||||
export interface Collab {
|
||||
started?: boolean
|
||||
error?: boolean
|
||||
room?: string
|
||||
y?: YOptions
|
||||
started?: boolean;
|
||||
error?: boolean;
|
||||
room?: string;
|
||||
y?: YOptions;
|
||||
}
|
||||
|
||||
export type LoadingType = 'loading' | 'initialized'
|
||||
|
||||
export interface State {
|
||||
text?: ProseMirrorState
|
||||
editorView?: EditorView
|
||||
extensions?: ProseMirrorExtension[]
|
||||
markdown?: boolean
|
||||
lastModified?: Date
|
||||
drafts: Draft[]
|
||||
config: Config
|
||||
error?: ErrorObject
|
||||
loading: LoadingType
|
||||
fullscreen?: boolean
|
||||
collab?: Collab
|
||||
path?: string
|
||||
args?: Args
|
||||
isMac?: boolean
|
||||
text?: ProseMirrorState;
|
||||
editorView?: any;
|
||||
extensions?: ProseMirrorExtension[];
|
||||
markdown?: boolean;
|
||||
lastModified?: Date;
|
||||
files: File[];
|
||||
config: Config;
|
||||
error?: ErrorObject;
|
||||
loading: LoadingType;
|
||||
fullscreen: boolean;
|
||||
collab?: Collab;
|
||||
path?: string;
|
||||
args?: Args;
|
||||
}
|
||||
|
||||
export interface Draft {
|
||||
text?: { [key: string]: any }
|
||||
body?: string
|
||||
lastModified?: Date
|
||||
path?: string
|
||||
markdown?: boolean
|
||||
extensions?: ProseMirrorExtension[]
|
||||
export interface File {
|
||||
text?: { [key: string]: any };
|
||||
lastModified?: string;
|
||||
path?: string;
|
||||
markdown?: boolean;
|
||||
}
|
||||
|
||||
export class ServiceError extends Error {
|
||||
public errorObject: ErrorObject
|
||||
constructor(id: string, props: any) {
|
||||
constructor(id: string, props: unknown) {
|
||||
super(id)
|
||||
this.errorObject = { id, props }
|
||||
}
|
||||
|
@ -90,7 +85,7 @@ export const useState = () => useContext(StateContext)
|
|||
|
||||
export const newState = (props: Partial<State> = {}): State => ({
|
||||
extensions: [],
|
||||
drafts: [],
|
||||
files: [],
|
||||
loading: 'loading',
|
||||
fullscreen: false,
|
||||
markdown: false,
|
||||
|
@ -100,7 +95,7 @@ export const newState = (props: Partial<State> = {}): State => ({
|
|||
font: 'muller',
|
||||
fontSize: 24,
|
||||
contentWidth: 800,
|
||||
// typewriterMode: true,
|
||||
typewriterMode: true,
|
||||
prettier: {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
.article__title {
|
||||
@include font-size(2.4rem);
|
||||
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
|
@ -36,7 +35,6 @@
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@include font-size(1.4rem);
|
||||
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
.error {
|
||||
button {
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
font-family: Muller;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
background: none;
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--foreground);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: var(--primary-foreground);
|
||||
border: 0;
|
||||
background: var(--primary-background);
|
||||
button {
|
||||
height: 50px;
|
||||
padding: 0 20px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
font-family: 'JetBrains Mono';
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
background: none;
|
||||
font-family: 'Muller';
|
||||
color: var(--foreground);
|
||||
border: 1px solid var(--foreground);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
color: var(--primary-foreground);
|
||||
border: 0;
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
|
|
@ -1,73 +1,67 @@
|
|||
@import './Button';
|
||||
@import './Sidebar';
|
||||
|
||||
.editor {
|
||||
margin: 0.5em;
|
||||
padding: 1em;
|
||||
min-width: 50%;
|
||||
min-height: fit-content;
|
||||
display: inline-block;
|
||||
border: 1px dashed rgb(0 0 0 / 80%);
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(0, 100, 200);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: rgb(0, 80, 160);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
-webkit-padding: 0.4em 0;
|
||||
padding: 0.4em;
|
||||
margin: 0 0 0.5em 0;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
a {
|
||||
color: rgb(0 100 200);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: rgb(0 100 200 / 70%);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: 0.4em;
|
||||
margin: 0 0 0.5em;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
color: #333;
|
||||
background-color: #f4f4f4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
color: var(--foreground);
|
||||
background-color: var(--background);
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
outline: none;
|
||||
margin: 1em 1em 1em 2em;
|
||||
|
@ -120,17 +114,15 @@
|
|||
}
|
||||
|
||||
blockquote {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
margin: 1.5em 0;
|
||||
border-left: 2px solid;
|
||||
@include font-size(1.6rem);
|
||||
margin: 1.5em 0;
|
||||
padding-left: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
font-size: small;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
> * {
|
||||
|
@ -160,10 +152,15 @@
|
|||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
@ -187,11 +184,11 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown::after {
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: '';
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid draftcurrentcolor;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
|
@ -209,7 +206,6 @@
|
|||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
|
||||
/* min-width: 6em; */
|
||||
}
|
||||
|
||||
|
@ -227,11 +223,11 @@
|
|||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label::after {
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: '';
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid draftcurrentcolor;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
|
@ -272,6 +268,7 @@
|
|||
border-bottom: 1px solid silver;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
@ -286,7 +283,7 @@
|
|||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: draftcurrentcolor;
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
|
@ -306,6 +303,10 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
@ -319,7 +320,7 @@ li.ProseMirror-selectednode {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode::after {
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
|
@ -349,7 +350,7 @@ li.ProseMirror-selectednode::after {
|
|||
|
||||
.ProseMirror-prompt {
|
||||
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;
|
||||
position: absolute;
|
||||
}
|
||||
|
@ -394,7 +395,7 @@ li.ProseMirror-selectednode::after {
|
|||
|
||||
.tooltip {
|
||||
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;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
display: flex;
|
||||
font-family: 'JetBrains Mono';
|
||||
justify-content: center;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
@ -18,7 +17,7 @@
|
|||
}
|
||||
|
||||
.error pre {
|
||||
background: var(--foreground);
|
||||
background: var(--foreground) 19;
|
||||
border: 1px solid var(--foreground);
|
||||
white-space: pre-wrap;
|
||||
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,19 +1,18 @@
|
|||
.layout--editor {
|
||||
.layout {
|
||||
display: flex;
|
||||
font-family: Muller;
|
||||
font-family: 'Muller';
|
||||
font-size: 18px;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
border-color: var(--background);
|
||||
min-height: 100vh;
|
||||
margin-top: -2.2rem !important;
|
||||
|
||||
&.dark {
|
||||
background: var(--foreground);
|
||||
color: var(--background);
|
||||
border-color: var(--foreground);
|
||||
}
|
||||
|
||||
|
||||
.drop-cursor {
|
||||
height: 2px !important;
|
||||
opacity: 0.5;
|
||||
|
|
|
@ -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 {
|
||||
background: #1f1f1f;
|
||||
height: 100%;
|
||||
|
@ -31,7 +44,7 @@
|
|||
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");
|
||||
content: '';
|
||||
height: 18px;
|
||||
|
@ -60,8 +73,7 @@
|
|||
}
|
||||
|
||||
.sidebar-label {
|
||||
color: var(--foreground);
|
||||
|
||||
color: var(--foreground) 7f;
|
||||
> i {
|
||||
text-transform: none;
|
||||
}
|
||||
|
@ -71,7 +83,6 @@
|
|||
margin: 10px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.sidebar-container button,
|
||||
.sidebar-container a,
|
||||
.sidebar-item {
|
||||
|
@ -81,7 +92,8 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 24px;
|
||||
font-family: Muller;
|
||||
font-family: 'Muller';
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-container a,
|
||||
|
@ -92,22 +104,8 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
@include font-size(120%);
|
||||
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
|
@ -140,12 +138,12 @@
|
|||
}
|
||||
|
||||
&[disabled] {
|
||||
color: var(--foreground);
|
||||
color: var(--foreground) 99;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.file {
|
||||
color: rgb(255 255 255 / 50%);
|
||||
color: rgba(255,255,255,0.5);
|
||||
line-height: 1.4;
|
||||
margin: 0 0 1em 1.5em;
|
||||
width: calc(100% - 2rem);
|
||||
|
@ -178,22 +176,20 @@
|
|||
}
|
||||
|
||||
.theme-switcher {
|
||||
border-bottom: 1px solid rgb(255 255 255 / 30%);
|
||||
border-top: 1px solid rgb(255 255 255 / 30%);
|
||||
border-bottom: 1px solid rgba(255,255,255,0.3);
|
||||
border-top: 1px solid rgba(255,255,255,0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 1rem;
|
||||
padding: 1em 0;
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type=checkbox] {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
|
||||
+ 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")
|
||||
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;
|
||||
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,
|
||||
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;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
|
@ -204,7 +200,7 @@
|
|||
transition: background-color 0.3s;
|
||||
width: 46px;
|
||||
|
||||
&::before {
|
||||
&:before {
|
||||
background-color: #fff;
|
||||
border-radius: 100%;
|
||||
content: '';
|
||||
|
@ -220,7 +216,7 @@
|
|||
&:checked + label {
|
||||
background-color: #fff;
|
||||
|
||||
&::before {
|
||||
&:before {
|
||||
background-color: #1f1f1f;
|
||||
left: 24px;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user