diff --git a/editors/editoria/src/Editoria.js b/editors/editoria/src/Editoria.js index cf9c690d2b67e40cf1d04fb586d9964b90001ec2..d1ec4b25547a00b440fc75dac04856a8c6e2a6ba 100644 --- a/editors/editoria/src/Editoria.js +++ b/editors/editoria/src/Editoria.js @@ -48,7 +48,7 @@ const Editoria = () => ( autoFocus placeholder="Type Something..." fileUpload={file => renderImage(file)} - value="<p> <span style='font-style:italic;'>test</span>hello <code> this is the code</code></p>" + value="<h1> <span style='font-style:italic;'>test</span>hello <code> this is the code</code></p>" layout={EditoriaLayout} user={user} /> diff --git a/editors/editoria/src/config/config.js b/editors/editoria/src/config/config.js index 1f1e3859de6379d7041663578482dde01fda862f..b31f1704dfe11f6fce63fff51731b7004f1c139a 100644 --- a/editors/editoria/src/config/config.js +++ b/editors/editoria/src/config/config.js @@ -17,7 +17,8 @@ import { ImageToolGroupService, TextBlockLevelService, TextToolGroupService, - TrackChangeService + TrackChangeService, + NoteService } from "wax-prosemirror-services"; import invisibles, { @@ -65,6 +66,7 @@ export default { new ImageToolGroupService(), new TextBlockLevelService(), new TextToolGroupService(), - new TrackChangeService() + new TrackChangeService(), + new NoteService() ] }; diff --git a/wax-prosemirror-core/src/FootnoteView.js b/wax-prosemirror-core/src/FootnoteView.js new file mode 100644 index 0000000000000000000000000000000000000000..807409849eb3d5d8ac8128577128dc655800b3a8 --- /dev/null +++ b/wax-prosemirror-core/src/FootnoteView.js @@ -0,0 +1,119 @@ +import { StepMap } from "prosemirror-transform"; +import { keymap } from "prosemirror-keymap"; +import { undo, redo } from "prosemirror-history"; +import { EditorView } from "prosemirror-view"; +import { EditorState } from "prosemirror-state"; +import { Schema } from "prosemirror-model"; +import { DefaultSchema } from "wax-prosemirror-schema"; + +class FootnoteView { + constructor(node, view, getPos) { + console.log(node); + // We'll need these later + this.node = node; + + this.outerView = view; + this.getPos = getPos; + + // The node's representation in the editor (empty, for now) + this.dom = document.createElement("footnote"); + // These are used when the footnote is selected + this.innerView = null; + } + selectNode() { + this.dom.classList.add("ProseMirror-selectednode"); + + if (!this.innerView) this.open(); + } + + deselectNode() { + this.dom.classList.remove("ProseMirror-selectednode"); + if (this.innerView) this.close(); + } + + open() { + // Append a tooltip to the outer node + let tooltip = this.dom.appendChild(document.createElement("div")); + tooltip.className = "footnote-tooltip"; + // And put a sub-ProseMirror into that + this.innerView = new EditorView(tooltip, { + // You can use any node as an editor document + state: EditorState.create({ + doc: this.node, + plugins: [ + keymap({ + "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch), + "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch) + }) + ] + }), + // This is the magic part + dispatchTransaction: this.dispatchInner.bind(this), + handleDOMEvents: { + mousedown: () => { + // Kludge to prevent issues due to the fact that the whole + // footnote is node-selected (and thus DOM-selected) when + // the parent editor is focused. + if (this.outerView.hasFocus()) this.innerView.focus(); + } + } + }); + } + + close() { + this.innerView.destroy(); + this.innerView = null; + this.dom.textContent = ""; + } + dispatchInner(tr) { + let { state, transactions } = this.innerView.state.applyTransaction(tr); + this.innerView.updateState(state); + + if (!tr.getMeta("fromOutside")) { + let outerTr = this.outerView.state.tr, + offsetMap = StepMap.offset(this.getPos() + 1); + for (let i = 0; i < transactions.length; i++) { + let steps = transactions[i].steps; + for (let j = 0; j < steps.length; j++) + outerTr.step(steps[j].map(offsetMap)); + } + if (outerTr.docChanged) this.outerView.dispatch(outerTr); + } + } + update(node) { + console.log("update"); + if (!node.sameMarkup(this.node)) return false; + this.node = node; + if (this.innerView) { + let state = this.innerView.state; + let start = node.content.findDiffStart(state.doc.content); + if (start != null) { + let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); + let overlap = start - Math.min(endA, endB); + if (overlap > 0) { + endA += overlap; + endB += overlap; + } + this.innerView.dispatch( + state.tr + .replace(start, endB, node.slice(start, endA)) + .setMeta("fromOutside", true) + ); + } + } + return true; + } + destroy() { + if (this.innerView) this.close(); + } + + stopEvent(event) { + return this.innerView && this.innerView.dom.contains(event.target); + } + + ignoreMutation() { + return true; + } +} + +export default FootnoteView; diff --git a/wax-prosemirror-core/src/WaxView.js b/wax-prosemirror-core/src/WaxView.js index 03af5111ef506dca908b270d4c661a53fc76a5f6..1dbb4005c354c4baef8772ee2df276591fc07676 100644 --- a/wax-prosemirror-core/src/WaxView.js +++ b/wax-prosemirror-core/src/WaxView.js @@ -13,6 +13,7 @@ import "prosemirror-view/style/prosemirror.css"; import trackedTransaction from "./track-changes/trackedTransaction"; import { WaxContext } from "./ioc-react"; +import FootnoteView from "./FootnoteView"; export default props => { const { readonly, onBlur, options, debug, autoFocus } = props; @@ -45,6 +46,11 @@ export default props => { } : null } + // nodeViews: { + // footnote(node, view, getPos) { + // return new FootnoteView(node, view, getPos); + // } + // } } ); context.updateView(view); diff --git a/wax-prosemirror-layouts/src/layouts/EditorElements.js b/wax-prosemirror-layouts/src/layouts/EditorElements.js index 0e771f795ab3ad4cfd99e82b2d46b83fc579db99..2955279069485125598e6bbccc98758b450f0f8e 100644 --- a/wax-prosemirror-layouts/src/layouts/EditorElements.js +++ b/wax-prosemirror-layouts/src/layouts/EditorElements.js @@ -2,7 +2,52 @@ import styled, { css } from "styled-components"; /* All styles regarding ProseMirror surface and elements */ -export default css`{ +export default css` + .ProseMirror footnote { + display: inline-block; + position: relative; + cursor: pointer; + } + + .ProseMirror footnote::after { + content: counter(footnote); + vertical-align: super; + font-size: 75%; + counter-increment: footnote; + } + + .ProseMirror-hideselection .footnote-tooltip *::selection { + background-color: transparent; + } + + .ProseMirror-hideselection .footnote-tooltip *::-moz-selection { + background-color: transparent; + } + + .ProseMirror .footnote-tooltip { + cursor: auto; + position: absolute; + left: -30px; + top: calc(100% + 10px); + background: silver; + padding: 3px; + border-radius: 2px; + width: 500px; + } + + .ProseMirror .footnote-tooltip::before { + border: 5px solid silver; + border-top-width: 0; + border-left-color: transparent; + border-right-color: transparent; + position: absolute; + top: -5px; + left: 27px; + content: " "; + height: 0; + width: 0; + } + .ProseMirror { -moz-box-shadow: 0 0 3px #ccc; -webkit-box-shadow: 0 0 3px #ccc; diff --git a/wax-prosemirror-services/index.js b/wax-prosemirror-services/index.js index 0434a1450d482241eda66ff463708461183f0335..d16ab4d6cb1784c1aac4624b104d98b35d44f434 100644 --- a/wax-prosemirror-services/index.js +++ b/wax-prosemirror-services/index.js @@ -1,15 +1,11 @@ export { default as MenuService } from "./src/MenuService/MenuService"; export { default as LinkService } from "./src/LinkService/LinkService"; -export { - default as PlaceholderService -} from "./src/PlaceholderService/PlaceholderService"; +export { default as PlaceholderService } from "./src/PlaceholderService/PlaceholderService"; export { default as ImageService } from "./src/ImageService/ImageService"; export { default as RulesService } from "./src/RulesService/RulesService"; export { default as SchemaService } from "./src/SchemaService/SchemaService"; -export { - default as ShortCutsService -} from "./src/ShortCutsService/ShortCutsService"; +export { default as ShortCutsService } from "./src/ShortCutsService/ShortCutsService"; export { default as OverlayService } from "./src/OverlayService/OverlayService"; export { default as Tool } from "./src/lib/Tools"; @@ -21,39 +17,20 @@ export { All Elements services */ export { default as BaseService } from "./src/BaseService/BaseService"; -export { - default as InlineAnnotationsService -} from "./src/InlineAnnotations/InlineAnnotationsService"; +export { default as InlineAnnotationsService } from "./src/InlineAnnotations/InlineAnnotationsService"; export { default as ListsService } from "./src/ListsService/ListsService"; export { default as TablesService } from "./src/TablesService/TablesService"; -export { - default as TextBlockLevelService -} from "./src/TextBlockLevel/TextBlockLevelService"; -export { - default as DisplayBlockLevelService -} from "./src/DisplayBlockLevel/DisplayBlockLevelService"; +export { default as TextBlockLevelService } from "./src/TextBlockLevel/TextBlockLevelService"; +export { default as DisplayBlockLevelService } from "./src/DisplayBlockLevel/DisplayBlockLevelService"; /* ToolGroups */ -export { - default as BaseToolGroupService -} from "./src/WaxToolGroups/BaseToolGroupService/BaseToolGroupService"; -export { - default as AnnotationToolGroupService -} from "./src/WaxToolGroups/AnnotationToolGroupService/AnnotationToolGroupService"; -export { - default as ListToolGroupService -} from "./src/WaxToolGroups/ListToolGroupService/ListToolGroupService"; -export { - default as ImageToolGroupService -} from "./src/WaxToolGroups/ImageToolGroupService/ImageToolGroupService"; -export { - default as TableToolGroupService -} from "./src/WaxToolGroups/TableToolGroupService/TableToolGroupService"; -export { - default as DisplayToolGroupService -} from "./src/WaxToolGroups/DisplayToolGroupService/DisplayToolGroupService"; -export { - default as TextToolGroupService -} from "./src/WaxToolGroups/TextToolGroupService/TextToolGroupService"; +export { default as BaseToolGroupService } from "./src/WaxToolGroups/BaseToolGroupService/BaseToolGroupService"; +export { default as AnnotationToolGroupService } from "./src/WaxToolGroups/AnnotationToolGroupService/AnnotationToolGroupService"; +export { default as ListToolGroupService } from "./src/WaxToolGroups/ListToolGroupService/ListToolGroupService"; +export { default as ImageToolGroupService } from "./src/WaxToolGroups/ImageToolGroupService/ImageToolGroupService"; +export { default as TableToolGroupService } from "./src/WaxToolGroups/TableToolGroupService/TableToolGroupService"; +export { default as DisplayToolGroupService } from "./src/WaxToolGroups/DisplayToolGroupService/DisplayToolGroupService"; +export { default as TextToolGroupService } from "./src/WaxToolGroups/TextToolGroupService/TextToolGroupService"; +export { default as NoteService } from "./src/NoteService/NoteService"; diff --git a/wax-prosemirror-services/src/NoteService/Editor.js b/wax-prosemirror-services/src/NoteService/Editor.js new file mode 100644 index 0000000000000000000000000000000000000000..894e940b1216241be492081d62ae4e88b4eb73d8 --- /dev/null +++ b/wax-prosemirror-services/src/NoteService/Editor.js @@ -0,0 +1,93 @@ +import React, { useEffect, useRef } from "react"; +import { EditorView } from "prosemirror-view"; +import { EditorState } from "prosemirror-state"; +import { StepMap } from "prosemirror-transform"; +import { keymap } from "prosemirror-keymap"; +import { undo, redo } from "prosemirror-history"; +import { markActive } from "../lib/Utils"; + +let noteView = null; +export default ({ node, view, pos }) => { + const editorRef = useRef(); + useEffect(() => { + console.log("test", node); + noteView = new EditorView( + { mount: editorRef.current }, + { + // You can use any node as an editor document + state: EditorState.create({ + doc: node, + plugins: [ + keymap({ + "Mod-z": () => undo(view.state, view.dispatch), + "Mod-y": () => redo(view.state, view.dispatch), + "Mod-u": () => + markActive(noteView.state.config.schema.marks.underline)( + noteView.state + ) + }) + ] + }), + // This is the magic part + dispatchTransaction: tr => { + console.log("in disaptch"); + let { state, transactions } = noteView.state.applyTransaction(tr); + noteView.updateState(state); + + if (!tr.getMeta("fromOutside")) { + let outerTr = view.state.tr, + offsetMap = StepMap.offset(pos + 1); + for (let i = 0; i < transactions.length; i++) { + let steps = transactions[i].steps; + for (let j = 0; j < steps.length; j++) + outerTr.step(steps[j].map(offsetMap)); + } + console.log(noteView.docView.node); + // outerTr.setNodeMarkup(pos, view.state.schema.nodes.footnote, { + // title: noteView.docView.node.textContent + // }); + + if (outerTr.docChanged) { + view.dispatch(outerTr); + } + } + }, + handleDOMEvents: { + mousedown: () => { + // Kludge to prevent issues due to the fact that the whole + // footnote is node-selected (and thus DOM-selected) when + // the parent editor is focused. + if (noteView.hasFocus()) noteView.focus(); + } + } + } + ); + }, []); + + if (noteView) { + let state = noteView.state; + let start = node.content.findDiffStart(state.doc.content); + console.log(start, node); + if (start != null) { + let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); + let overlap = start - Math.min(endA, endB); + if (overlap > 0) { + endA += overlap; + endB += overlap; + } + console.log(endA, endB); + noteView.dispatch( + state.tr + .replace(start, endB, node.slice(start, endA)) + .setMeta("fromOutside", true) + ); + } + } + + return ( + <div + style={{ height: "100px", border: "1px solid black" }} + ref={editorRef} + ></div> + ); +}; diff --git a/wax-prosemirror-services/src/NoteService/Note.js b/wax-prosemirror-services/src/NoteService/Note.js new file mode 100644 index 0000000000000000000000000000000000000000..c270e7a5bc79e46eb5361787f24b585dd4bd5137 --- /dev/null +++ b/wax-prosemirror-services/src/NoteService/Note.js @@ -0,0 +1,18 @@ +import Tools from "../lib/Tools"; +import { injectable } from "inversify"; +import { icons } from "wax-prosemirror-components"; + +@injectable() +export default class Note extends Tools { + title = "Insert Note"; + content = icons.footnote; + + get run() { + return (state, dispatch) => { + const footnote = state.config.schema.nodes.footnote.create(); + dispatch(state.tr.replaceSelectionWith(footnote)); + }; + } + + get enable() {} +} diff --git a/wax-prosemirror-services/src/NoteService/NoteComponent.js b/wax-prosemirror-services/src/NoteService/NoteComponent.js new file mode 100644 index 0000000000000000000000000000000000000000..45801a38dd75527ba12521f0995f427dd2ca5c44 --- /dev/null +++ b/wax-prosemirror-services/src/NoteService/NoteComponent.js @@ -0,0 +1,63 @@ +import React, { useContext, useState, useEffect, useMemo } from "react"; +import { WaxContext } from "wax-prosemirror-core/src/ioc-react"; +import { isEqual } from "lodash"; +import NoteEditor from "./NoteEditor"; + +export default () => { + const { view } = useContext(WaxContext); + const [notes, setNotes] = useState([]); + + useEffect(() => { + setNotes(updateNotes(view)); + }, [JSON.stringify(updateNotes(view))]); + + const noteComponent = useMemo( + () => <NoteEditor notes={notes} view={view} />, + [notes] + ); + + return <div>{noteComponent}</div>; +}; + +const updateNotes = view => { + if (view) { + return findBlockNodes( + view.state.doc, + view.state.schema.nodes.footnote, + true + ); + } + return []; +}; + +export const flatten = (node, descend = true) => { + if (!node) { + throw new Error('Invalid "node" parameter'); + } + const result = []; + node.descendants((child, pos) => { + result.push({ node: child, pos }); + if (!descend) { + return false; + } + }); + return result; +}; + +export const findChildren = (node, predicate, descend) => { + if (!node) { + throw new Error('Invalid "node" parameter'); + } else if (!predicate) { + throw new Error('Invalid "predicate" parameter'); + } + return flatten(node, descend).filter(child => { + // predicate(child.node)console.log(child.node); + // return predicate(child.node); + // console.log(child.node.type.name === "footnote", predicate(child.node)); + return child.node.type.name === "footnote" ? child.node : false; + }); +}; + +export const findBlockNodes = (node, descend) => { + return findChildren(node, child => child.isBlock, descend); +}; diff --git a/wax-prosemirror-services/src/NoteService/NoteEditor.js b/wax-prosemirror-services/src/NoteService/NoteEditor.js new file mode 100644 index 0000000000000000000000000000000000000000..05094be4cd8ddecef3c182c61c2e2a59dcc83dd9 --- /dev/null +++ b/wax-prosemirror-services/src/NoteService/NoteEditor.js @@ -0,0 +1,12 @@ +import React from "react"; +import Editor from "./Editor"; + +export default ({ notes, view }) => { + return ( + <div> + {notes.map(note => ( + <Editor node={note.node} pos={note.pos} view={view} /> + ))} + </div> + ); +}; diff --git a/wax-prosemirror-services/src/NoteService/NoteService.js b/wax-prosemirror-services/src/NoteService/NoteService.js new file mode 100644 index 0000000000000000000000000000000000000000..57ef932615d4f15a42192950aad26b8ad71a0ead --- /dev/null +++ b/wax-prosemirror-services/src/NoteService/NoteService.js @@ -0,0 +1,18 @@ +import Note from "./Note"; +import Service from "wax-prosemirror-core/src/services/Service"; +import NoteComponent from "./NoteComponent"; + +class NoteService extends Service { + name = "NoteService"; + + boot() { + const layout = this.container.get("Layout"); + layout.addComponent("bottomBar", NoteComponent); + } + + register() { + this.container.bind("Note").to(Note); + } +} + +export default NoteService; diff --git a/wax-prosemirror-services/src/OverlayService/usePosition.js b/wax-prosemirror-services/src/OverlayService/usePosition.js index 90f7e71d0a227d4e9496175d9df648c7082a5481..ec33f2e6d7e8d683383adfdda8bf492977f7f951 100644 --- a/wax-prosemirror-services/src/OverlayService/usePosition.js +++ b/wax-prosemirror-services/src/OverlayService/usePosition.js @@ -12,7 +12,7 @@ const defaultOverlay = { }; export default options => { - let { view } = useContext(WaxContext); + const { view } = useContext(WaxContext); const [position, setPosition] = useState({ position: "absolute", diff --git a/wax-prosemirror-services/src/SchemaService/DefaultSchema.js b/wax-prosemirror-services/src/SchemaService/DefaultSchema.js index 3bcc0cac0f0121db5e5e79c32fa82d337b74e0c5..73541c6d32767d87ecc45286d7e547d4e8bffb51 100644 --- a/wax-prosemirror-services/src/SchemaService/DefaultSchema.js +++ b/wax-prosemirror-services/src/SchemaService/DefaultSchema.js @@ -47,7 +47,31 @@ export default { const attrs = blockLevelToDOM(node); return ["p", attrs, 0]; } + }, + footnote: { + group: "inline", + content: "inline*", + inline: true, + // This makes the view treat the node as a leaf, even though it + // technically has content + atom: true, + toDOM: () => ["footnote"], + parseDOM: [{ tag: "footnote" }] } + // footnote: { + // group: "inline", + // content: "block+", + // inline: true, + // atom: true, + // toDOM: dom => { + // return ["footnote"]; + // }, + // parseDOM: [ + // { + // tag: "footnote" + // } + // ] + // }, }, marks: {} }; diff --git a/wax-prosemirror-services/src/WaxToolGroups/BaseToolGroupService/Base.js b/wax-prosemirror-services/src/WaxToolGroups/BaseToolGroupService/Base.js index d9d35019ad83c726d610b45c19c7255c06a7b7fd..1b3e2c89dc170ee0d6ee2f16004910bfadb0a5de 100644 --- a/wax-prosemirror-services/src/WaxToolGroups/BaseToolGroupService/Base.js +++ b/wax-prosemirror-services/src/WaxToolGroups/BaseToolGroupService/Base.js @@ -4,9 +4,13 @@ import ToolGroup from "../../lib/ToolGroup"; @injectable() class Base extends ToolGroup { tools = []; - constructor(@inject("Undo") undo, @inject("Redo") redo) { + constructor( + @inject("Undo") undo, + @inject("Redo") redo, + @inject("Note") note + ) { super(); - this.tools = [undo, redo]; + this.tools = [undo, redo, note]; } }