From 9e690514f0075cc56981c5711af5e429f67ad584 Mon Sep 17 00:00:00 2001 From: Yuxi Wang Date: Fri, 19 Aug 2022 11:14:53 -0700 Subject: [PATCH 01/42] Add NewTopBlock --- packages/editor/demo/init.tsx | 53 ++++++++----------- packages/editor/demo/snippet.html | 11 +++- packages/editor/src/prosemirror/index.ts | 1 + packages/editor/src/views/LinkBlockView.tsx | 2 +- packages/editor/src/views/NewTopBlock.tsx | 48 +++++++++++++++++ packages/editor/src/views/NodeView.tsx | 15 ++++-- packages/editor/src/views/index.ts | 2 + packages/editor/styles/index.scss | 13 +++++ packages/schema/src/nodes/basic.ts | 18 ++++++- packages/schema/src/nodes/types.ts | 1 + packages/schema/src/schemas.ts | 3 +- .../schema/src/serialize/markdown/index.ts | 3 ++ packages/schema/src/serialize/tex/index.ts | 3 ++ 13 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 packages/editor/src/views/NewTopBlock.tsx diff --git a/packages/editor/demo/init.tsx b/packages/editor/demo/init.tsx index 3eb41bae..b2a882ae 100644 --- a/packages/editor/demo/init.tsx +++ b/packages/editor/demo/init.tsx @@ -5,23 +5,14 @@ import { Button, createTheme } from '@material-ui/core'; import { toHTML, toMarkdown, toTex, ReferenceKind, process, toText } from '@curvenote/schema'; import { Sidenote, AnchorBase } from 'sidenotes'; import { Fragment } from 'prosemirror-model'; -import { - actions, - Editor, - EditorMenu, - Store, - setup, - Suggestions, - Attributes, - InlineActions, - LinkResult, -} from '../src'; +import type { Store, LinkResult } from '../src'; +import { actions, Editor, EditorMenu, setup, Suggestions, Attributes, InlineActions } from '../src'; import rootReducer from './reducers'; import middleware from './middleware'; import 'codemirror/lib/codemirror.css'; import '../styles/index.scss'; import 'sidenotes/dist/sidenotes.css'; -import { Options } from '../src/connect'; +import type { Options } from '../src/connect'; import SuggestionSwitch from '../src/components/Suggestion/Switch'; import InlineActionSwitch from '../src/components/InlineActions/Switch'; @@ -112,8 +103,8 @@ export function DemoEditor({ content, store = createStore() }: { content: string store.dispatch(actions.initEditorState('full', stateKey, true, content, 0)); store.subscribe(() => { const myst = document.getElementById('myst'); - const text = document.getElementById('text'); - const tex = document.getElementById('tex'); + // const text = document.getElementById('text'); + // const tex = document.getElementById('tex'); const html = document.getElementById('html'); const editor = store.getState().editor.state.editors[stateKey]; if (myst) { @@ -123,34 +114,34 @@ export function DemoEditor({ content, store = createStore() }: { content: string myst.innerText = 'Error converting to markdown'; } } - if (tex) { - try { - tex.innerText = toTex(editor.state.doc); - } catch (error) { - tex.innerText = 'There was an error :('; - } - } - if (text) { - try { - text.innerText = toText(editor.state.doc); - } catch (error) { - text.innerText = 'There was an error :('; - } - } + // if (tex) { + // try { + // tex.innerText = toTex(editor.state.doc); + // } catch (error) { + // tex.innerText = 'There was an error :('; + // } + // } + // if (text) { + // try { + // text.innerText = toText(editor.state.doc); + // } catch (error) { + // text.innerText = 'There was an error :('; + // } + // } if (html) { html.innerText = toHTML(editor.state.doc, editor.state.schema, document); } // Update the counter const counts = process.countState(editor.state); - const words = process.countWords(editor.state); + // const words = process.countWords(editor.state); const updates = { 'count-sec': `${counts.sec.all.length} (${counts.sec.total})`, 'count-fig': `${counts.fig.all.length} (${counts.fig.total})`, 'count-eq': `${counts.eq.all.length} (${counts.eq.total})`, 'count-code': `${counts.code.all.length} (${counts.code.total})`, 'count-table': `${counts.table.all.length} (${counts.table.total})`, - 'count-words': `${words.words}`, - 'count-char': `${words.characters_including_spaces} (${words.characters_excluding_spaces})`, + // 'count-words': `${words.words}`, + // 'count-char': `${words.characters_including_spaces} (${words.characters_excluding_spaces})`, }; Object.entries(updates).forEach(([key, count]) => { const el = document.getElementById(key); diff --git a/packages/editor/demo/snippet.html b/packages/editor/demo/snippet.html index 932b9787..18ad8d3c 100644 --- a/packages/editor/demo/snippet.html +++ b/packages/editor/demo/snippet.html @@ -1,4 +1,7 @@ -

Welcome to @curvenote/editor!

+
+

Welcome to @curvenote/editor!

+
+

Feel free to ➡️ edit this document ⬅️, or for collaborative editing, check out curvenote.com, which is a scientific writing platform that @@ -32,6 +35,8 @@

Welcome to @curvenote/editor!

for code or ``` for a code block!

+
+

You can also configure the editor to show citations, like this one from Geophysics . Try @@ -80,7 +85,9 @@

You can insert code:

You can insert footnotes:

This is a footnote👋. You can access it using the /footnote commandOr the menu under the plus!

+
+
+ diff --git a/packages/editor/src/prosemirror/index.ts b/packages/editor/src/prosemirror/index.ts index 8e66938a..96642d34 100644 --- a/packages/editor/src/prosemirror/index.ts +++ b/packages/editor/src/prosemirror/index.ts @@ -54,6 +54,7 @@ export function createEditorView( iframe: views.IFrameView, link: views.LinkView, link_block: views.createLinkBlockView, + newTopNode: views.createTopBlockView, time: views.TimeView, mention: views.MentionView, button: views.newWidgetView, diff --git a/packages/editor/src/views/LinkBlockView.tsx b/packages/editor/src/views/LinkBlockView.tsx index 0fee3b01..dbcdd749 100644 --- a/packages/editor/src/views/LinkBlockView.tsx +++ b/packages/editor/src/views/LinkBlockView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NodeViewProps } from './types'; +import type { NodeViewProps } from './types'; import createNodeView from './NodeView'; function LinkBlock({ node }: NodeViewProps) { diff --git a/packages/editor/src/views/NewTopBlock.tsx b/packages/editor/src/views/NewTopBlock.tsx new file mode 100644 index 00000000..436f28a3 --- /dev/null +++ b/packages/editor/src/views/NewTopBlock.tsx @@ -0,0 +1,48 @@ +import type { Node } from 'prosemirror-model'; +import type { EditorView } from 'prosemirror-view'; +import { isEditable } from '../prosemirror/plugins/editable'; +import type { GetPos } from './types'; + +class NewTopBlockNodeView { + // The node's representation in the editor (empty, for now) + dom: HTMLDivElement; + contentDOM: HTMLDivElement; + + node: Node; + + view: EditorView; + + getPos?: GetPos; + + constructor(node: Node, view: EditorView, getPos: GetPos) { + this.node = node; + this.view = view; + this.getPos = getPos; + this.dom = document.createElement('div'); + + const blockControls = document.createElement('div'); + blockControls.setAttribute('contenteditable', 'false'); + blockControls.innerText = 'block controls'; + blockControls.classList.add('block-controls'); + this.dom.appendChild(blockControls); + + const contentContainer = document.createElement('div'); + this.dom.appendChild(contentContainer); + + this.dom.classList.add('yoo'); + this.contentDOM = contentContainer; // tells prosemirror to render children here + } + + selectNode() { + if (!isEditable(this.view.state)) return; + this.dom.classList.add('ProseMirror-selectednode'); + } + + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } +} + +export function createTopBlockView(node: Node, view: EditorView, getPos: GetPos) { + return new NewTopBlockNodeView(node, view, getPos); +} diff --git a/packages/editor/src/views/NodeView.tsx b/packages/editor/src/views/NodeView.tsx index 30edaf23..683f2a91 100644 --- a/packages/editor/src/views/NodeView.tsx +++ b/packages/editor/src/views/NodeView.tsx @@ -1,17 +1,18 @@ import React, { Component } from 'react'; import { render } from 'react-dom'; -import { Node } from 'prosemirror-model'; -import { EditorView } from 'prosemirror-view'; +import type { Node } from 'prosemirror-model'; +import type { EditorView } from 'prosemirror-view'; import { ThemeProvider } from '@material-ui/core'; import { Provider } from 'react-redux'; import { isEditable } from '../prosemirror/plugins/editable'; import { opts, ref } from '../connect'; -import { GetPos, NodeViewProps } from './types'; +import type { GetPos, NodeViewProps } from './types'; export type Options = { wrapper: 'span' | 'div'; className?: string; enableSelectionHighlight?: boolean; + containsChildren?: boolean; }; export type ClassWrapperProps = { @@ -55,6 +56,8 @@ class ClassWrapper extends Component { export class ReactWrapper { dom: HTMLElement; + contentDOM?: HTMLElement | null; + node: Node; view: EditorView; @@ -76,8 +79,12 @@ export class ReactWrapper { this.getPos = getPos; this.isSelectionHighlightEnabled = !!options.enableSelectionHighlight; this.dom = document.createElement(options.wrapper); + if (options.containsChildren) { + this.contentDOM = this.dom; + } if (options.className) this.dom.classList.add(options.className); + console.log('this', this); render( @@ -128,7 +135,7 @@ export class ReactWrapper { function createNodeView( Editor: React.FunctionComponent, - options: Options = { wrapper: 'div' }, + options: Options = { wrapper: 'div', containsChildren: false }, ) { return (node: Node, view: EditorView, getPos: boolean | GetPos) => new ReactWrapper(Editor, { node, view, getPos: getPos as GetPos }, options); diff --git a/packages/editor/src/views/index.ts b/packages/editor/src/views/index.ts index d23760cf..9d855940 100644 --- a/packages/editor/src/views/index.ts +++ b/packages/editor/src/views/index.ts @@ -6,6 +6,7 @@ import { createLinkBlockView } from './LinkBlockView'; import { TimeView } from './TimeView'; import { MentionView } from './Mention'; import { CodeBlockView } from './CodeBlockView'; +import { createTopBlockView } from './NewTopBlock'; import { FootnoteView } from './FootnoteView'; import createNodeView from './NodeView'; import WidgetView, { newWidgetView } from './WidgetView'; @@ -23,6 +24,7 @@ export default { ImageView, IFrameView, LinkView, + createTopBlockView, createLinkBlockView, TimeView, WidgetView, diff --git a/packages/editor/styles/index.scss b/packages/editor/styles/index.scss index 35419cc2..622b4f47 100644 --- a/packages/editor/styles/index.scss +++ b/packages/editor/styles/index.scss @@ -29,3 +29,16 @@ display: none !important; } } + +.yoo { + padding: 8px 16px; + margin: 8px 0px; + border-radius: 4px; + border: 1px solid gray; +} + +.block-controls { + background-color: black; + padding: 12px; + color: white +} diff --git a/packages/schema/src/nodes/basic.ts b/packages/schema/src/nodes/basic.ts index 7ff00a4c..0f48ee9e 100644 --- a/packages/schema/src/nodes/basic.ts +++ b/packages/schema/src/nodes/basic.ts @@ -19,7 +19,8 @@ import { NodeGroups } from './types'; import { nodeNames } from '../types'; export const doc: NodeSpec = { - content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, + // content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, + content: `${NodeGroups.newBlock}+`, }; export const docParagraph: NodeSpec = { @@ -30,6 +31,21 @@ export const docComment: NodeSpec = { content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${nodeNames.equation})+`, // browsers will completely collapse the node when it's empty `+` is necessary }; +export const newTopNode = { + attrs: {}, + content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, + group: NodeGroups.newBlock, + + parseDOM: [ + { + tag: 'div.new-block', + }, + ], + + toDOM() { + return ['div', { class: 'new-block' }, 0]; + }, +}; export const paragraph: MyNodeSpec = { attrs: {}, content: `${NodeGroups.inline}*`, diff --git a/packages/schema/src/nodes/types.ts b/packages/schema/src/nodes/types.ts index 85700e6c..32148ced 100644 --- a/packages/schema/src/nodes/types.ts +++ b/packages/schema/src/nodes/types.ts @@ -23,6 +23,7 @@ export enum ReferenceKind { export enum NodeGroups { 'top' = 'topblock', 'block' = 'block', + 'newBlock' = 'newBlock', 'heading' = 'heading', 'blockOrHeading' = '(block | heading)+', 'blockOrEquation' = '(block | equation)+', diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index bcd2b520..6d37b02c 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -49,6 +49,7 @@ export const reactiveNodes = { export const nodes = { // Basic markdown doc: basic.doc, + newTopNode: basic.newTopNode, text: basic.text, paragraph: basic.paragraph, heading: Nodes.Heading.default, @@ -129,7 +130,7 @@ export function getSchema(useSchema: UseSchema) { if (typeof useSchema === 'string') { switch (useSchema) { case 'full': - return new Schema(presets.full); + return new Schema(presets.full as any); case 'paragraph': return new Schema(presets.paragraph); case 'comment': diff --git a/packages/schema/src/serialize/markdown/index.ts b/packages/schema/src/serialize/markdown/index.ts index 3b656d23..ef38d353 100644 --- a/packages/schema/src/serialize/markdown/index.ts +++ b/packages/schema/src/serialize/markdown/index.ts @@ -19,6 +19,9 @@ const mdNodes: MarkdownSerializerParameters[0] = { text(state, node) { state.text(cleanWhitespaceChars(node.text ?? '')); }, + newTopNode(state, node) { + state.text('not supported!!'); + }, paragraph(state, node) { state.renderInline(node); state.closeBlock(node); diff --git a/packages/schema/src/serialize/tex/index.ts b/packages/schema/src/serialize/tex/index.ts index 169e7edb..c83cc7f9 100644 --- a/packages/schema/src/serialize/tex/index.ts +++ b/packages/schema/src/serialize/tex/index.ts @@ -90,6 +90,9 @@ export const texSerializer = new MarkdownSerializer( }, image: nodes.Image.toTex, figure: nodes.Figure.toTex, + newTopNode(state, node) { + state.text('not supported!!'); + }, figcaption: nodes.Figcaption.toTex, footnote: nodes.Footnote.toTex, iframe: blankTexLines, From c9add62bc15e0849d383247bc492cc5d21b031e7 Mon Sep 17 00:00:00 2001 From: Yuxi Wang Date: Mon, 22 Aug 2022 11:37:13 -0700 Subject: [PATCH 02/42] Add new top block nodeView --- packages/editor/src/prosemirror/index.ts | 4 +- packages/editor/src/views/Block.tsx | 131 ++++++++++++++++++ packages/editor/src/views/NewTopBlock.tsx | 48 ------- packages/editor/src/views/index.ts | 2 +- packages/editor/styles/index.scss | 10 +- packages/schema/src/index.ts | 2 + packages/schema/src/nodes/basic.ts | 4 +- packages/schema/src/nodes/types.ts | 2 +- packages/schema/src/schemas.ts | 2 +- .../schema/src/serialize/markdown/index.ts | 3 +- packages/schema/src/serialize/tex/index.ts | 3 +- 11 files changed, 147 insertions(+), 64 deletions(-) create mode 100644 packages/editor/src/views/Block.tsx delete mode 100644 packages/editor/src/views/NewTopBlock.tsx diff --git a/packages/editor/src/prosemirror/index.ts b/packages/editor/src/prosemirror/index.ts index 96642d34..a2c2bc8a 100644 --- a/packages/editor/src/prosemirror/index.ts +++ b/packages/editor/src/prosemirror/index.ts @@ -1,4 +1,4 @@ -import { schemas, fromHTML } from '@curvenote/schema'; +import { schemas, fromHTML, BLOCK_NODE_NAME } from '@curvenote/schema'; import type { Transaction } from 'prosemirror-state'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; @@ -54,7 +54,7 @@ export function createEditorView( iframe: views.IFrameView, link: views.LinkView, link_block: views.createLinkBlockView, - newTopNode: views.createTopBlockView, + [BLOCK_NODE_NAME]: views.createTopBlockView, time: views.TimeView, mention: views.MentionView, button: views.newWidgetView, diff --git a/packages/editor/src/views/Block.tsx b/packages/editor/src/views/Block.tsx new file mode 100644 index 00000000..a4b3d6fe --- /dev/null +++ b/packages/editor/src/views/Block.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import type { Node } from 'prosemirror-model'; +import { render } from 'react-dom'; +import type { NodeView, EditorView } from 'prosemirror-view'; +import { isEditable } from '../prosemirror/plugins/editable'; +import type { GetPos } from './types'; +import { TextSelection } from 'prosemirror-state'; +import { createStyles, makeStyles } from '@material-ui/core'; +import { BLOCK_NODE_NAME } from '@curvenote/schema'; + +function addBlock(view: EditorView, node: Node, getPos: GetPos, before: boolean) { + const blockPos = getPos(); + const { state, dispatch } = view; + const blockNode = state.schema.nodes[BLOCK_NODE_NAME]; + // create a new node before pos + const paragraph = state.schema.nodes.paragraph.createAndFill({}) as Node; + const newNode = blockNode.createAndFill({}, [paragraph]) as Node; + + const tr = before + ? state.tr.insert(blockPos, newNode) + : state.tr.insert(blockPos + node.content.size + 1, newNode); + + dispatch(tr); + + if (before) { + const resolvedPos = view.state.tr.doc.resolve(getPos() - 1); + view.dispatch(view.state.tr.setSelection(new TextSelection(resolvedPos))); + } else { + const resolvedPos = view.state.tr.doc.resolve(getPos() + node.content.size + 3); + view.dispatch(view.state.tr.setSelection(new TextSelection(resolvedPos))); + } + + view.focus(); +} + +const useStyles = makeStyles(() => + createStyles({ + root: { + cursor: 'all-scroll', + backgroundColor: 'black', + padding: '8px', + color: 'white', + }, + }), +); + +function FancyBlockControls({ + view, + node, + getPos, +}: { + view: EditorView; + node: Node; + getPos: GetPos; +}) { + console.log('rendered', node.content.size); + const classes = useStyles(); + return ( +
+ + +
+ ); +} + +class NewTopBlockNodeView implements NodeView { + // The node's representation in the editor (empty, for now) + dom: HTMLDivElement; + contentDOM: HTMLDivElement; + blockControls: HTMLDivElement; + + node: Node; + + view: EditorView; + + getPos?: GetPos; + + constructor(node: Node, view: EditorView, getPos: GetPos) { + this.node = node; + this.view = view; + this.getPos = getPos; + this.dom = document.createElement('div'); + + const blockControls = document.createElement('div'); + this.blockControls = blockControls; + blockControls.setAttribute('contenteditable', 'false'); + console.log('consturction??'); + render(, this.blockControls); + this.dom.appendChild(blockControls); + + const contentContainer = document.createElement('div'); + this.dom.appendChild(contentContainer); + + this.dom.classList.add('block-node-view'); + this.contentDOM = contentContainer; // tells prosemirror to render children here + } + + selectNode() { + if (!isEditable(this.view.state)) return; + this.dom.classList.add('ProseMirror-selectednode'); + } + + deselectNode() { + this.dom.classList.remove('ProseMirror-selectednode'); + } + + update(node: Node) { + if (!this.view || !this.getPos) return false; + render( + , + this.blockControls, + ); + return true; + } +} + +export function createTopBlockView(node: Node, view: EditorView, getPos: GetPos) { + return new NewTopBlockNodeView(node, view, getPos); +} diff --git a/packages/editor/src/views/NewTopBlock.tsx b/packages/editor/src/views/NewTopBlock.tsx deleted file mode 100644 index 436f28a3..00000000 --- a/packages/editor/src/views/NewTopBlock.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { Node } from 'prosemirror-model'; -import type { EditorView } from 'prosemirror-view'; -import { isEditable } from '../prosemirror/plugins/editable'; -import type { GetPos } from './types'; - -class NewTopBlockNodeView { - // The node's representation in the editor (empty, for now) - dom: HTMLDivElement; - contentDOM: HTMLDivElement; - - node: Node; - - view: EditorView; - - getPos?: GetPos; - - constructor(node: Node, view: EditorView, getPos: GetPos) { - this.node = node; - this.view = view; - this.getPos = getPos; - this.dom = document.createElement('div'); - - const blockControls = document.createElement('div'); - blockControls.setAttribute('contenteditable', 'false'); - blockControls.innerText = 'block controls'; - blockControls.classList.add('block-controls'); - this.dom.appendChild(blockControls); - - const contentContainer = document.createElement('div'); - this.dom.appendChild(contentContainer); - - this.dom.classList.add('yoo'); - this.contentDOM = contentContainer; // tells prosemirror to render children here - } - - selectNode() { - if (!isEditable(this.view.state)) return; - this.dom.classList.add('ProseMirror-selectednode'); - } - - deselectNode() { - this.dom.classList.remove('ProseMirror-selectednode'); - } -} - -export function createTopBlockView(node: Node, view: EditorView, getPos: GetPos) { - return new NewTopBlockNodeView(node, view, getPos); -} diff --git a/packages/editor/src/views/index.ts b/packages/editor/src/views/index.ts index 9d855940..8b6267e2 100644 --- a/packages/editor/src/views/index.ts +++ b/packages/editor/src/views/index.ts @@ -6,7 +6,7 @@ import { createLinkBlockView } from './LinkBlockView'; import { TimeView } from './TimeView'; import { MentionView } from './Mention'; import { CodeBlockView } from './CodeBlockView'; -import { createTopBlockView } from './NewTopBlock'; +import { createTopBlockView } from './Block'; import { FootnoteView } from './FootnoteView'; import createNodeView from './NodeView'; import WidgetView, { newWidgetView } from './WidgetView'; diff --git a/packages/editor/styles/index.scss b/packages/editor/styles/index.scss index 622b4f47..369660e0 100644 --- a/packages/editor/styles/index.scss +++ b/packages/editor/styles/index.scss @@ -30,15 +30,9 @@ } } -.yoo { - padding: 8px 16px; +.block-node-view { + padding: 12px 16px; margin: 8px 0px; border-radius: 4px; border: 1px solid gray; } - -.block-controls { - background-color: black; - padding: 12px; - color: white -} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 8576448b..6714798e 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -15,3 +15,5 @@ export * as server from './server'; export * as process from './process'; export { DEFAULT_FORMAT, DEFAULT_IMAGE_WIDTH } from './defaults'; + +export { BLOCK_NODE_NAME } from './nodes/basic'; diff --git a/packages/schema/src/nodes/basic.ts b/packages/schema/src/nodes/basic.ts index 0f48ee9e..636bf222 100644 --- a/packages/schema/src/nodes/basic.ts +++ b/packages/schema/src/nodes/basic.ts @@ -31,8 +31,10 @@ export const docComment: NodeSpec = { content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${nodeNames.equation})+`, // browsers will completely collapse the node when it's empty `+` is necessary }; -export const newTopNode = { +export const BLOCK_NODE_NAME = 'newTopBlock'; // NOTE: This cannot be named block unless we can rename nodeGroup.block +export const block: NodeSpec = { attrs: {}, + draggable: true, content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, group: NodeGroups.newBlock, diff --git a/packages/schema/src/nodes/types.ts b/packages/schema/src/nodes/types.ts index 32148ced..acf191b1 100644 --- a/packages/schema/src/nodes/types.ts +++ b/packages/schema/src/nodes/types.ts @@ -21,9 +21,9 @@ export enum ReferenceKind { } export enum NodeGroups { + 'newBlock' = 'newBlock', // NOTE: naming here needs some work (requirements: avoid collision) 'top' = 'topblock', 'block' = 'block', - 'newBlock' = 'newBlock', 'heading' = 'heading', 'blockOrHeading' = '(block | heading)+', 'blockOrEquation' = '(block | equation)+', diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index 6d37b02c..6f679a39 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -49,7 +49,7 @@ export const reactiveNodes = { export const nodes = { // Basic markdown doc: basic.doc, - newTopNode: basic.newTopNode, + [basic.BLOCK_NODE_NAME]: basic.block, text: basic.text, paragraph: basic.paragraph, heading: Nodes.Heading.default, diff --git a/packages/schema/src/serialize/markdown/index.ts b/packages/schema/src/serialize/markdown/index.ts index ef38d353..2d95b99a 100644 --- a/packages/schema/src/serialize/markdown/index.ts +++ b/packages/schema/src/serialize/markdown/index.ts @@ -2,6 +2,7 @@ import type { MarkdownSerializer } from 'prosemirror-markdown'; import { MarkdownSerializerState } from 'prosemirror-markdown'; import type { Node as ProsemirrorNode } from 'prosemirror-model'; import { isPlainURL, backticksFor, wrapMark } from './utils'; +import { BLOCK_NODE_NAME } from '../../nodes/basic'; import * as nodes from '../../nodes'; import type { MarkdownOptions, MdSerializerState } from '../types'; import { cleanWhitespaceChars } from '../clean'; @@ -19,7 +20,7 @@ const mdNodes: MarkdownSerializerParameters[0] = { text(state, node) { state.text(cleanWhitespaceChars(node.text ?? '')); }, - newTopNode(state, node) { + [BLOCK_NODE_NAME](state, node) { state.text('not supported!!'); }, paragraph(state, node) { diff --git a/packages/schema/src/serialize/tex/index.ts b/packages/schema/src/serialize/tex/index.ts index c83cc7f9..0e79a976 100644 --- a/packages/schema/src/serialize/tex/index.ts +++ b/packages/schema/src/serialize/tex/index.ts @@ -13,6 +13,7 @@ import { nodeNames } from '../../types'; import type { TexOptions, TexSerializerState } from '../types'; import { TexFormatTypes } from '../types'; import { getIndent } from '../indent'; +import { BLOCK_NODE_NAME } from '../../nodes/basic'; function createMarkOpenClose(name?: string) { return { @@ -90,7 +91,7 @@ export const texSerializer = new MarkdownSerializer( }, image: nodes.Image.toTex, figure: nodes.Figure.toTex, - newTopNode(state, node) { + [BLOCK_NODE_NAME](state, node) { state.text('not supported!!'); }, figcaption: nodes.Figcaption.toTex, From 03e44c4d5bba3f159a8e322791912f603e78bc63 Mon Sep 17 00:00:00 2001 From: Yuxi Wang Date: Mon, 22 Aug 2022 20:27:19 -0700 Subject: [PATCH 03/42] Refactor schema naming --- packages/editor/src/prosemirror/index.ts | 4 +- packages/editor/src/views/Block.tsx | 64 ++++++++++++++----- packages/editor/styles/index.scss | 13 ++++ packages/schema/src/index.ts | 2 - packages/schema/src/nodes/basic.ts | 21 +++--- packages/schema/src/nodes/code.ts | 2 +- packages/schema/src/nodes/figure.ts | 2 +- packages/schema/src/nodes/footnote.ts | 5 +- packages/schema/src/nodes/iframe.ts | 2 +- packages/schema/src/nodes/image.ts | 2 +- packages/schema/src/nodes/types.ts | 12 ++-- packages/schema/src/schemas.ts | 3 +- .../schema/src/serialize/markdown/index.ts | 4 +- packages/schema/src/serialize/tex/index.ts | 3 +- packages/schema/src/serialize/text/index.ts | 2 + packages/schema/src/types.ts | 1 + 16 files changed, 95 insertions(+), 47 deletions(-) diff --git a/packages/editor/src/prosemirror/index.ts b/packages/editor/src/prosemirror/index.ts index a2c2bc8a..dd4c1324 100644 --- a/packages/editor/src/prosemirror/index.ts +++ b/packages/editor/src/prosemirror/index.ts @@ -1,4 +1,4 @@ -import { schemas, fromHTML, BLOCK_NODE_NAME } from '@curvenote/schema'; +import { schemas, fromHTML, BLOCK_NODE_NAME, nodeNames } from '@curvenote/schema'; import type { Transaction } from 'prosemirror-state'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; @@ -54,7 +54,7 @@ export function createEditorView( iframe: views.IFrameView, link: views.LinkView, link_block: views.createLinkBlockView, - [BLOCK_NODE_NAME]: views.createTopBlockView, + [nodeNames.block]: views.createTopBlockView, time: views.TimeView, mention: views.MentionView, button: views.newWidgetView, diff --git a/packages/editor/src/views/Block.tsx b/packages/editor/src/views/Block.tsx index a4b3d6fe..65aaa007 100644 --- a/packages/editor/src/views/Block.tsx +++ b/packages/editor/src/views/Block.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import type { Node } from 'prosemirror-model'; import { render } from 'react-dom'; import type { NodeView, EditorView } from 'prosemirror-view'; @@ -6,12 +6,13 @@ import { isEditable } from '../prosemirror/plugins/editable'; import type { GetPos } from './types'; import { TextSelection } from 'prosemirror-state'; import { createStyles, makeStyles } from '@material-ui/core'; -import { BLOCK_NODE_NAME } from '@curvenote/schema'; +import { nodeNames } from '@curvenote/schema'; +import classNames from 'classnames'; function addBlock(view: EditorView, node: Node, getPos: GetPos, before: boolean) { const blockPos = getPos(); const { state, dispatch } = view; - const blockNode = state.schema.nodes[BLOCK_NODE_NAME]; + const blockNode = state.schema.nodes[nodeNames.block]; // create a new node before pos const paragraph = state.schema.nodes.paragraph.createAndFill({}) as Node; const newNode = blockNode.createAndFill({}, [paragraph]) as Node; @@ -36,9 +37,15 @@ function addBlock(view: EditorView, node: Node, getPos: GetPos, before: boolean) const useStyles = makeStyles(() => createStyles({ root: { + display: 'none', + flexDirection: 'column', + justifyContent: 'space-between', + height: '100%', + position: 'absolute', + top: 0, + left: -15, cursor: 'all-scroll', backgroundColor: 'black', - padding: '8px', color: 'white', }, }), @@ -48,38 +55,43 @@ function FancyBlockControls({ view, node, getPos, + selected, }: { view: EditorView; node: Node; getPos: GetPos; + selected?: boolean; }) { - console.log('rendered', node.content.size); const classes = useStyles(); + console.log('render', selected); return ( -
+
); } -class NewTopBlockNodeView implements NodeView { +class BlockNodeView implements NodeView { // The node's representation in the editor (empty, for now) dom: HTMLDivElement; contentDOM: HTMLDivElement; blockControls: HTMLDivElement; + state: { selected: boolean }; node: Node; @@ -92,6 +104,7 @@ class NewTopBlockNodeView implements NodeView { this.view = view; this.getPos = getPos; this.dom = document.createElement('div'); + this.state = { selected: false }; // let's treat this immutably shall we const blockControls = document.createElement('div'); this.blockControls = blockControls; @@ -107,13 +120,19 @@ class NewTopBlockNodeView implements NodeView { this.contentDOM = contentContainer; // tells prosemirror to render children here } - selectNode() { - if (!isEditable(this.view.state)) return; - this.dom.classList.add('ProseMirror-selectednode'); - } - deselectNode() { + if (!this.getPos) return; this.dom.classList.remove('ProseMirror-selectednode'); + this.state = { selected: false }; + render( + , + this.blockControls, + ); } update(node: Node) { @@ -124,8 +143,23 @@ class NewTopBlockNodeView implements NodeView { ); return true; } + selectNode() { + console.log('selectNode', this.getPos); + if (!isEditable(this.view.state) || !this.getPos) return; + this.dom.classList.add('ProseMirror-selectednode'); + this.state = { selected: true }; + render( + , + this.blockControls, + ); + } } export function createTopBlockView(node: Node, view: EditorView, getPos: GetPos) { - return new NewTopBlockNodeView(node, view, getPos); + return new BlockNodeView(node, view, getPos); } diff --git a/packages/editor/styles/index.scss b/packages/editor/styles/index.scss index 369660e0..709bfe8c 100644 --- a/packages/editor/styles/index.scss +++ b/packages/editor/styles/index.scss @@ -35,4 +35,17 @@ margin: 8px 0px; border-radius: 4px; border: 1px solid gray; + position: relative; + &:hover { + .block-controls { + display: flex; + } + } + } + + + .selected-block-control { + background-color: grey; + display: 'flex'; + } diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index 6714798e..8576448b 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -15,5 +15,3 @@ export * as server from './server'; export * as process from './process'; export { DEFAULT_FORMAT, DEFAULT_IMAGE_WIDTH } from './defaults'; - -export { BLOCK_NODE_NAME } from './nodes/basic'; diff --git a/packages/schema/src/nodes/basic.ts b/packages/schema/src/nodes/basic.ts index 636bf222..83f81de8 100644 --- a/packages/schema/src/nodes/basic.ts +++ b/packages/schema/src/nodes/basic.ts @@ -20,7 +20,7 @@ import { nodeNames } from '../types'; export const doc: NodeSpec = { // content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, - content: `${NodeGroups.newBlock}+`, + content: `${NodeGroups.block}+`, }; export const docParagraph: NodeSpec = { @@ -28,15 +28,14 @@ export const docParagraph: NodeSpec = { }; export const docComment: NodeSpec = { - content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${nodeNames.equation})+`, // browsers will completely collapse the node when it's empty `+` is necessary + content: `(${NodeGroups.content} | ${NodeGroups.heading} | ${nodeNames.equation})+`, // browsers will completely collapse the node when it's empty `+` is necessary }; -export const BLOCK_NODE_NAME = 'newTopBlock'; // NOTE: This cannot be named block unless we can rename nodeGroup.block export const block: NodeSpec = { attrs: {}, draggable: true, - content: `(${NodeGroups.block} | ${NodeGroups.heading} | ${NodeGroups.top})+`, - group: NodeGroups.newBlock, + content: `(${NodeGroups.content} | ${NodeGroups.heading} | ${NodeGroups.top})+`, + group: NodeGroups.block, parseDOM: [ { @@ -51,7 +50,7 @@ export const block: NodeSpec = { export const paragraph: MyNodeSpec = { attrs: {}, content: `${NodeGroups.inline}*`, - group: NodeGroups.block, + group: NodeGroups.content, parseDOM: [{ tag: 'p' }], toDOM() { return ['p', 0]; @@ -65,8 +64,8 @@ export const paragraph: MyNodeSpec = { export const blockquote: MyNodeSpec = { attrs: {}, - content: `${NodeGroups.block}+`, - group: NodeGroups.block, + content: `${NodeGroups.content}+`, + group: NodeGroups.content, defining: true, parseDOM: [{ tag: 'blockquote' }], toDOM() { @@ -82,7 +81,7 @@ export const blockquote: MyNodeSpec = { /** Horizontal rule */ export const horizontal_rule: MyNodeSpec = { attrs: {}, - group: NodeGroups.block, + group: NodeGroups.content, parseDOM: [{ tag: 'hr' }], toDOM() { return ['hr', { class: 'break' }]; @@ -110,8 +109,8 @@ export const hard_break: MyNodeSpec = { const listNodes = addListNodes( OrderedMap.from({}), - `paragraph ${NodeGroups.block}*`, - NodeGroups.block, + `paragraph ${NodeGroups.content}*`, + NodeGroups.content, ) as OrderedMap>; export type OrderedListAttrs = { diff --git a/packages/schema/src/nodes/code.ts b/packages/schema/src/nodes/code.ts index 46d08cc8..63ee8415 100644 --- a/packages/schema/src/nodes/code.ts +++ b/packages/schema/src/nodes/code.ts @@ -69,7 +69,7 @@ const code_block: MyNodeSpec = { }, content: `${NodeGroups.text}*`, marks: '', - group: NodeGroups.block, + group: NodeGroups.content, code: true, defining: true, parseDOM: [ diff --git a/packages/schema/src/nodes/figure.ts b/packages/schema/src/nodes/figure.ts index 295205dc..d2f6b101 100644 --- a/packages/schema/src/nodes/figure.ts +++ b/packages/schema/src/nodes/figure.ts @@ -31,7 +31,7 @@ export type Attrs = NumberedNode & { }; const figure: MyNodeSpec = { - group: NodeGroups.block, + group: NodeGroups.content, content: NodeGroups.insideFigure, isolating: true, attrs: { diff --git a/packages/schema/src/nodes/footnote.ts b/packages/schema/src/nodes/footnote.ts index c36badf4..b4d051f3 100644 --- a/packages/schema/src/nodes/footnote.ts +++ b/packages/schema/src/nodes/footnote.ts @@ -2,11 +2,12 @@ import type { FlowContent, InlineFootnote, NoAttrs } from '../spec'; import type { MdFormatSerialize, TexFormatSerialize } from '../serialize/types'; import type { MyNodeSpec, Props } from './types'; import { NodeGroups } from './types'; +import { nodeNames } from '../types'; const footnote: MyNodeSpec = { attrs: {}, - group: 'inline', - content: `(${NodeGroups.text} | math)*`, + group: NodeGroups.inline, + content: `(${NodeGroups.text} | ${nodeNames.math})*`, inline: true, draggable: true, // This makes the view treat the node as a leaf, even though it diff --git a/packages/schema/src/nodes/iframe.ts b/packages/schema/src/nodes/iframe.ts index a1ed62fc..133114cb 100644 --- a/packages/schema/src/nodes/iframe.ts +++ b/packages/schema/src/nodes/iframe.ts @@ -18,7 +18,7 @@ const iframe: MyNodeSpec = { align: { default: 'center' }, width: { default: DEFAULT_IMAGE_WIDTH }, }, - group: NodeGroups.block, + group: NodeGroups.content, draggable: true, parseDOM: [ { diff --git a/packages/schema/src/nodes/image.ts b/packages/schema/src/nodes/image.ts index cb23fa6d..5074bacb 100644 --- a/packages/schema/src/nodes/image.ts +++ b/packages/schema/src/nodes/image.ts @@ -30,7 +30,7 @@ const image: MyNodeSpec = { align: { default: 'center' }, // Deprecated, use figure caption: { default: false }, // Deprecated, use figcaption }, - group: NodeGroups.block, + group: NodeGroups.content, draggable: true, parseDOM: [ { diff --git a/packages/schema/src/nodes/types.ts b/packages/schema/src/nodes/types.ts index acf191b1..17c70f0f 100644 --- a/packages/schema/src/nodes/types.ts +++ b/packages/schema/src/nodes/types.ts @@ -21,13 +21,13 @@ export enum ReferenceKind { } export enum NodeGroups { - 'newBlock' = 'newBlock', // NOTE: naming here needs some work (requirements: avoid collision) - 'top' = 'topblock', - 'block' = 'block', + 'block' = 'block', // NOTE: naming here needs some work (requirements: avoid collision) + 'top' = 'topcontent', + 'content' = 'content', 'heading' = 'heading', - 'blockOrHeading' = '(block | heading)+', - 'blockOrEquation' = '(block | equation)+', - 'blockOrEquationOrHeading' = '(block | heading | equation)+', + 'blockOrHeading' = '(content | heading)+', + 'blockOrEquation' = '(content | equation)+', + 'blockOrEquationOrHeading' = '(content | heading | equation)+', 'inline' = 'inline', 'text' = 'text', 'cite' = 'cite', diff --git a/packages/schema/src/schemas.ts b/packages/schema/src/schemas.ts index 6f679a39..225e4338 100644 --- a/packages/schema/src/schemas.ts +++ b/packages/schema/src/schemas.ts @@ -4,6 +4,7 @@ import * as basic from './nodes/basic'; import { nodes as tableNodes } from './nodes/table'; import * as basicMarks from './marks'; import * as Nodes from './nodes'; +import { nodeNames } from './types'; export const listNodes = { ordered_list: basic.ordered_list, @@ -49,7 +50,7 @@ export const reactiveNodes = { export const nodes = { // Basic markdown doc: basic.doc, - [basic.BLOCK_NODE_NAME]: basic.block, + [nodeNames.block]: basic.block, text: basic.text, paragraph: basic.paragraph, heading: Nodes.Heading.default, diff --git a/packages/schema/src/serialize/markdown/index.ts b/packages/schema/src/serialize/markdown/index.ts index 2d95b99a..da0aae13 100644 --- a/packages/schema/src/serialize/markdown/index.ts +++ b/packages/schema/src/serialize/markdown/index.ts @@ -2,11 +2,11 @@ import type { MarkdownSerializer } from 'prosemirror-markdown'; import { MarkdownSerializerState } from 'prosemirror-markdown'; import type { Node as ProsemirrorNode } from 'prosemirror-model'; import { isPlainURL, backticksFor, wrapMark } from './utils'; -import { BLOCK_NODE_NAME } from '../../nodes/basic'; import * as nodes from '../../nodes'; import type { MarkdownOptions, MdSerializerState } from '../types'; import { cleanWhitespaceChars } from '../clean'; import { toMdastSnippet } from '../mdast'; +import { nodeNames } from '../../types'; type MarkdownSerializerParameters = ConstructorParameters; @@ -20,7 +20,7 @@ const mdNodes: MarkdownSerializerParameters[0] = { text(state, node) { state.text(cleanWhitespaceChars(node.text ?? '')); }, - [BLOCK_NODE_NAME](state, node) { + [nodeNames.block](state, node) { state.text('not supported!!'); }, paragraph(state, node) { diff --git a/packages/schema/src/serialize/tex/index.ts b/packages/schema/src/serialize/tex/index.ts index 0e79a976..0bd5001c 100644 --- a/packages/schema/src/serialize/tex/index.ts +++ b/packages/schema/src/serialize/tex/index.ts @@ -13,7 +13,6 @@ import { nodeNames } from '../../types'; import type { TexOptions, TexSerializerState } from '../types'; import { TexFormatTypes } from '../types'; import { getIndent } from '../indent'; -import { BLOCK_NODE_NAME } from '../../nodes/basic'; function createMarkOpenClose(name?: string) { return { @@ -91,7 +90,7 @@ export const texSerializer = new MarkdownSerializer( }, image: nodes.Image.toTex, figure: nodes.Figure.toTex, - [BLOCK_NODE_NAME](state, node) { + [nodeNames.block](state, node) { state.text('not supported!!'); }, figcaption: nodes.Figcaption.toTex, diff --git a/packages/schema/src/serialize/text/index.ts b/packages/schema/src/serialize/text/index.ts index aa7052e7..5cefdf21 100644 --- a/packages/schema/src/serialize/text/index.ts +++ b/packages/schema/src/serialize/text/index.ts @@ -3,6 +3,7 @@ import { MarkdownSerializer } from 'prosemirror-markdown'; import type { Node as ProsemirrorNode } from 'prosemirror-model'; import * as nodes from '../../nodes'; import { cleanWhitespaceChars } from '../clean'; +import { nodeNames } from '../../types'; function simpleInlineRender(state: MarkdownSerializerState, node: ProsemirrorNode) { state.renderInline(node); @@ -62,6 +63,7 @@ export const textSerializer = new MarkdownSerializer( figcaption: simpleBlockRender, time: nodes.Time.toMarkdown, // This is just the time! :) callout: simpleBlockRender, + [nodeNames.block]: simpleBlockRender, aside: simpleBlockRender, // Technical math: simpleInlineRender, diff --git a/packages/schema/src/types.ts b/packages/schema/src/types.ts index 025a5488..0bf0c9ec 100644 --- a/packages/schema/src/types.ts +++ b/packages/schema/src/types.ts @@ -7,6 +7,7 @@ export * from './serialize/types'; export type { LinkAttrs, AlignOptions }; export enum nodeNames { + block = 'block', text = 'text', paragraph = 'paragraph', heading = 'heading', From 7ef537f88565b4cf126ed1881bc4daa7f920fad2 Mon Sep 17 00:00:00 2001 From: Yuxi Wang Date: Wed, 24 Aug 2022 11:27:35 -0700 Subject: [PATCH 04/42] Add redux state in new block --- packages/editor/demo/snippet.html | 9 +- packages/editor/src/components/Editor.tsx | 19 +++- packages/editor/src/prosemirror/index.ts | 2 +- .../editor/src/prosemirror/plugins/index.ts | 3 + .../editor/src/prosemirror/plugins/state.ts | 104 ++++++++++++++++++ packages/editor/src/store/ui/actions.ts | 11 +- packages/editor/src/store/ui/reducers.ts | 10 +- packages/editor/src/store/ui/selectors.ts | 4 + packages/editor/src/store/ui/types.ts | 9 +- packages/editor/src/views/Block.tsx | 65 +++++------ packages/editor/styles/index.scss | 21 +++- packages/schema/src/nodes/basic.ts | 20 +++- 12 files changed, 224 insertions(+), 53 deletions(-) create mode 100644 packages/editor/src/prosemirror/plugins/state.ts diff --git a/packages/editor/demo/snippet.html b/packages/editor/demo/snippet.html index 18ad8d3c..a4ca4b26 100644 --- a/packages/editor/demo/snippet.html +++ b/packages/editor/demo/snippet.html @@ -1,7 +1,7 @@ -
+

Welcome to @curvenote/editor!

-
+

Feel free to ➡️ edit this document ⬅️, or for collaborative editing, check out curvenote.com, which is a scientific writing platform that @@ -36,7 +36,7 @@

Welcome to @curvenote/editor!

-
+

You can also configure the editor to show citations, like this one from Geophysics . Try @@ -87,7 +87,7 @@

You can insert footnotes:

This is a footnote👋. You can access it using the /footnote commandOr the menu under the plus!

-
+
+ diff --git a/packages/editor/src/components/Editor.tsx b/packages/editor/src/components/Editor.tsx index ba1a5810..c4d1d81b 100644 --- a/packages/editor/src/components/Editor.tsx +++ b/packages/editor/src/components/Editor.tsx @@ -1,20 +1,24 @@ import React, { useEffect, useRef } from 'react'; -import { EditorView } from 'prosemirror-view'; +import type { EditorView } from 'prosemirror-view'; import { useDispatch, useSelector } from 'react-redux'; -import { EditorState, Transaction } from 'prosemirror-state'; +import type { EditorState, Transaction } from 'prosemirror-state'; import { opts } from '../connect'; import { createEditorView } from '../prosemirror'; -import { Dispatch, State, actions, selectors } from '../store'; +import type { Dispatch, State } from '../store'; +import { actions, selectors } from '../store'; +import { findParentNodeOfTypeClosestToPos } from '@curvenote/prosemirror-utils'; +import { selectBlock } from '../store/actions'; type Props = { stateKey: any; viewId: string; className?: string; autoUnsubscribe?: boolean; + store?: any; }; const Editor = (props: Props) => { - const { stateKey, viewId, className, autoUnsubscribe } = props; + const { stateKey, viewId, className, autoUnsubscribe, store } = props; const dispatch = useDispatch(); @@ -38,7 +42,14 @@ const Editor = (props: Props) => { // Immidiately update the view. // This is important for properly handling selections. // Cannot use react event loop here. + editorView.current?.updateState(next); + + const parentBlock = findParentNodeOfTypeClosestToPos( + next.selection.$from, + next.schema.nodes.block, + ); + dispatch(selectBlock(parentBlock?.node.attrs.id || null)); }); editorView.current.dom.id = viewId; if (className) editorView.current.dom.classList.add(...className.split(' ')); diff --git a/packages/editor/src/prosemirror/index.ts b/packages/editor/src/prosemirror/index.ts index dd4c1324..88381d87 100644 --- a/packages/editor/src/prosemirror/index.ts +++ b/packages/editor/src/prosemirror/index.ts @@ -1,4 +1,4 @@ -import { schemas, fromHTML, BLOCK_NODE_NAME, nodeNames } from '@curvenote/schema'; +import { schemas, fromHTML, nodeNames } from '@curvenote/schema'; import type { Transaction } from 'prosemirror-state'; import { EditorState } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; diff --git a/packages/editor/src/prosemirror/plugins/index.ts b/packages/editor/src/prosemirror/plugins/index.ts index b55a044c..f7afb33f 100644 --- a/packages/editor/src/prosemirror/plugins/index.ts +++ b/packages/editor/src/prosemirror/plugins/index.ts @@ -20,6 +20,8 @@ import { handleSuggestion } from '../../store/suggestion/actions'; import commentsPlugin from './comments'; import { getImagePlaceholderPlugin } from './ImagePlaceholder'; import getPromptPlugin from './prompts'; +import { statePlugin } from './state'; +export { getPluginState as getStatePluginState } from './state'; function tablesPlugins(schema: Schema) { // Don't add plugins if they are not in the schema @@ -96,6 +98,7 @@ export function getPlugins( ]; } return [ + statePlugin(), editablePlugin(startEditable), getPromptPlugin(), ...autocomplete({ diff --git a/packages/editor/src/prosemirror/plugins/state.ts b/packages/editor/src/prosemirror/plugins/state.ts new file mode 100644 index 00000000..196a6933 --- /dev/null +++ b/packages/editor/src/prosemirror/plugins/state.ts @@ -0,0 +1,104 @@ +import { findParentNodeOfTypeClosestToPos } from '@curvenote/prosemirror-utils'; +import type { EditorState, Transaction } from 'prosemirror-state'; +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; + +const key = new PluginKey('state'); + +export const getPluginState = (state?: EditorState | null): boolean => { + if (state == null) return false; + const plugin = key.get(state); + return plugin?.getState(state) ?? false; +}; + +function getSelectedBlockState(state: EditorState) { + const result = findParentNodeOfTypeClosestToPos(state.selection.$from, state.schema.nodes.block); + return result; +} + +function getState(state: EditorState) { + return { + selectedBlock: getSelectedBlockState(state), + }; +} + +function findParentBlock(state: EditorState) { + return findParentNodeOfTypeClosestToPos(state.selection.$from, state.schema.nodes.block); +} + +function createDecorations(state: EditorState) { + const parentBlock = findParentBlock(state); + if (!parentBlock) return DecorationSet.empty; + // console.log('parentBlock', parentBlock, state.selection.$from); + return DecorationSet.create(state.doc, [ + Decoration.widget( + parentBlock.start, + (() => { + const dom = document.createElement('div'); + console.log('creating new node'); + dom.innerHTML = 'Decoration Block Controls?'; + return dom; + })(), + ), + ]); +} + +export const statePlugin = (): Plugin => { + const plugin: Plugin = new Plugin({ + key, + props: { + decorations(state) { + return this.getState(state).decorations; + }, + handleDOMEvents: { + mouseover(this, view, event) { + // console.log('mouseenter', view.posAtDOM(event.target as HTMLElement, 0)); + }, + mouseout(this, view, event) { + // console.log('mouseleave', view.posAtDOM(event.target as HTMLElement, 0)); + }, + }, + }, + state: { + init: (config, state) => { + const parentBlock = findParentBlock(state); + if (!parentBlock) return { decorations: DecorationSet.empty, parentBlock: null }; + // console.log('plugin init config', { + // decorations: createDecorations(state), + // parentBlock: parentBlock, + // }); + return { decorations: createDecorations(state), parentBlock: parentBlock }; + }, + apply(tr, value, oldState, newState) { + const prevPluginState: any = plugin.getState(oldState); + if (!tr.docChanged) { + return { + decorations: createDecorations(newState), + parentBlock: prevPluginState.parentBlock, + }; + } + const { parentBlock, decorations } = prevPluginState; + const newParentBlockSearchResult = findParentBlock(newState); + console.log('compare', newParentBlockSearchResult, parentBlock); + // TODO: how to compare whether the block has changed + if (newParentBlockSearchResult?.node.eq(parentBlock.node)) { + // update decorations + console.log('updating '); + return { + decorations: createDecorations(newState), + parentBlock, + }; + } + // create new decorations + return { + decorations: createDecorations(newState), + parentBlock: newParentBlockSearchResult, + }; + }, + toJSON(state) { + return state; + }, + }, + }); + return plugin; +}; diff --git a/packages/editor/src/store/ui/actions.ts b/packages/editor/src/store/ui/actions.ts index 90f624a9..3df9c5b6 100644 --- a/packages/editor/src/store/ui/actions.ts +++ b/packages/editor/src/store/ui/actions.ts @@ -1,6 +1,6 @@ import type { PopperPlacementType } from '@material-ui/core'; -import type { InlineSelection, UIActionTypes } from './types'; -import { SELECT_EDITOR_VIEW, INLINE_SELECTION, SelectionKinds } from './types'; +import type { InlineSelection, SelectBlockAction, UIActionTypes } from './types'; +import { SELECT_BLOCK, SELECT_EDITOR_VIEW, INLINE_SELECTION, SelectionKinds } from './types'; import type { AppThunk } from '../types'; import { getEditorUI, getInlineActionKind, getSelectedEditorAndViews } from './selectors'; import { getEditorView } from '../state/selectors'; @@ -16,6 +16,13 @@ export function updateSelectView(viewId: string | null): AppThunk { }; } +export function selectBlock(blockId: string | null): SelectBlockAction { + return { + type: SELECT_BLOCK, + payload: blockId, + }; +} + /** * @deprecated use updateSelectView */ diff --git a/packages/editor/src/store/ui/reducers.ts b/packages/editor/src/store/ui/reducers.ts index 48921f0b..099f55c4 100644 --- a/packages/editor/src/store/ui/reducers.ts +++ b/packages/editor/src/store/ui/reducers.ts @@ -1,15 +1,23 @@ import type { UIState, UIActionTypes } from './types'; -import { SELECT_EDITOR_VIEW, INLINE_SELECTION } from './types'; +import { SELECT_BLOCK, SELECT_EDITOR_VIEW, INLINE_SELECTION } from './types'; export const initialState: UIState = { stateId: null, viewId: null, selection: null, + selectedBlock: null, }; // eslint-disable-next-line @typescript-eslint/default-param-last const uiReducer = (state = initialState, action: UIActionTypes): UIState => { switch (action.type) { + case SELECT_BLOCK: { + if (action.payload === state.selectedBlock) return state; + return { + ...state, + selectedBlock: action.payload, + }; + } case SELECT_EDITOR_VIEW: { const { stateId, viewId } = action.payload; if (state.stateId === stateId && state.viewId === viewId) { diff --git a/packages/editor/src/store/ui/selectors.ts b/packages/editor/src/store/ui/selectors.ts index f2479810..3aa6470d 100644 --- a/packages/editor/src/store/ui/selectors.ts +++ b/packages/editor/src/store/ui/selectors.ts @@ -17,6 +17,10 @@ export const getEditorUIStateAndViewIds: (state: State) => { export function isInlineActionOpen(state: State) { return state.editor.ui.selection != null; } + +export function selectSelectedBlockId(state: State) { + return state.editor.ui.selectedBlock; +} export function getInlineActionAnchorEl(state: State) { return state.editor.ui.selection?.anchorEl ?? null; } diff --git a/packages/editor/src/store/ui/types.ts b/packages/editor/src/store/ui/types.ts index df3a69a6..c9ea644f 100644 --- a/packages/editor/src/store/ui/types.ts +++ b/packages/editor/src/store/ui/types.ts @@ -2,6 +2,7 @@ import type { PopperPlacementType } from '@material-ui/core'; export const SELECT_EDITOR_VIEW = 'SELECT_EDITOR_VIEW'; export const INLINE_SELECTION = 'INLINE_SELECTION'; +export const SELECT_BLOCK = 'EDITOR/SELECT_BLOCK'; export enum SelectionKinds { link = 'link', @@ -31,6 +32,7 @@ export type UIState = { stateId: string | null; viewId: string | null; selection: InlineSelection | null; + selectedBlock: string | null; }; export interface SelectEditorViewAction { @@ -46,4 +48,9 @@ export interface InlineSelectionAction { payload: InlineSelection | null; } -export type UIActionTypes = SelectEditorViewAction | InlineSelectionAction; +export interface SelectBlockAction { + type: typeof SELECT_BLOCK; + payload: string | null; +} + +export type UIActionTypes = SelectEditorViewAction | InlineSelectionAction | SelectBlockAction; diff --git a/packages/editor/src/views/Block.tsx b/packages/editor/src/views/Block.tsx index 65aaa007..e62c8fe5 100644 --- a/packages/editor/src/views/Block.tsx +++ b/packages/editor/src/views/Block.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import type { Node } from 'prosemirror-model'; import { render } from 'react-dom'; import type { NodeView, EditorView } from 'prosemirror-view'; @@ -8,6 +8,10 @@ import { TextSelection } from 'prosemirror-state'; import { createStyles, makeStyles } from '@material-ui/core'; import { nodeNames } from '@curvenote/schema'; import classNames from 'classnames'; +import { v4 } from 'uuid'; +import { ref } from '../connect'; +import { Provider, useSelector } from 'react-redux'; +import { selectSelectedBlockId } from '../store/selectors'; function addBlock(view: EditorView, node: Node, getPos: GetPos, before: boolean) { const blockPos = getPos(); @@ -15,7 +19,7 @@ function addBlock(view: EditorView, node: Node, getPos: GetPos, before: boolean) const blockNode = state.schema.nodes[nodeNames.block]; // create a new node before pos const paragraph = state.schema.nodes.paragraph.createAndFill({}) as Node; - const newNode = blockNode.createAndFill({}, [paragraph]) as Node; + const newNode = blockNode.createAndFill({ id: v4() }, [paragraph]) as Node; const tr = before ? state.tr.insert(blockPos, newNode) @@ -51,22 +55,25 @@ const useStyles = makeStyles(() => }), ); -function FancyBlockControls({ - view, - node, - getPos, - selected, -}: { +function Menu() { + return
Menu
; +} + +type Props = { view: EditorView; node: Node; getPos: GetPos; - selected?: boolean; -}) { +}; + +function FancyControl({ view, node, getPos }: Props) { const classes = useStyles(); - console.log('render', selected); + const selectedBlock = useSelector(selectSelectedBlockId); + console.log('selectedBlock', selectedBlock); return (
+ - +