Skip to content
Snippets Groups Projects
Commit 0c0268f2 authored by Giannis Kopanas's avatar Giannis Kopanas Committed by chris
Browse files

feat(notes): basic notes implementation

parent ad6d6a64
No related branches found
No related tags found
1 merge request!45Develop
Showing
with 424 additions and 43 deletions
......@@ -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}
/>
......
......@@ -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()
]
};
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;
......@@ -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);
......
......@@ -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;
......
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";
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>
);
};
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() {}
}
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);
};
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>
);
};
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;
......@@ -12,7 +12,7 @@ const defaultOverlay = {
};
export default options => {
let { view } = useContext(WaxContext);
const { view } = useContext(WaxContext);
const [position, setPosition] = useState({
position: "absolute",
......
......@@ -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: {}
};
......@@ -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];
}
}
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment