diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/Server.ts | 2 | ||||
-rw-r--r-- | src/client/util/RichTextSchema.tsx | 223 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.scss | 54 | ||||
-rw-r--r-- | src/client/util/TooltipTextMenu.tsx | 125 | ||||
-rw-r--r-- | src/client/views/InkingCanvas.scss | 32 | ||||
-rw-r--r-- | src/client/views/InkingCanvas.tsx | 161 | ||||
-rw-r--r-- | src/client/views/InkingControl.tsx | 77 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 66 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionFreeFormView.tsx | 6 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.scss | 2 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.scss | 16 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 14 | ||||
-rw-r--r-- | src/fields/InkField.ts | 47 | ||||
-rw-r--r-- | src/fields/KeyStore.ts | 1 | ||||
-rw-r--r-- | src/server/Message.ts | 2 | ||||
-rw-r--r-- | src/server/ServerUtil.ts | 30 |
17 files changed, 840 insertions, 20 deletions
diff --git a/src/client/Server.ts b/src/client/Server.ts index 2d162b93a..f0cf0bb9b 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -9,7 +9,7 @@ import { MessageStore, Types } from "./../server/Message"; export class Server { public static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap(); - static Socket: SocketIOClient.Socket = OpenSocket("http://localhost:1234"); + static Socket: SocketIOClient.Socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:1234`); static GUID: string = Utils.GenerateGuid() diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx new file mode 100644 index 000000000..abf448c9f --- /dev/null +++ b/src/client/util/RichTextSchema.tsx @@ -0,0 +1,223 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray } from "prosemirror-model" +import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands' +import { redo, undo } from 'prosemirror-history' +import { orderedList, bulletList, listItem } from 'prosemirror-schema-list' + +const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], + preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0] + +// :: Object +// [Specs](#model.NodeSpec) for the nodes defined in this schema. +export const nodes: { [index: string]: NodeSpec } = { + // :: NodeSpec The top level document node. + doc: { + content: "block+" + }, + + // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // as a `<p>` element. + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{ tag: "p" }], + toDOM() { return pDOM } + }, + + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. + blockquote: { + content: "block+", + group: "block", + defining: true, + parseDOM: [{ tag: "blockquote" }], + toDOM() { return blockquoteDOM } + }, + + // :: NodeSpec A horizontal rule (`<hr>`). + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + toDOM() { return hrDOM } + }, + + // :: NodeSpec A heading textblock, with a `level` attribute that + // should hold the number 1 to 6. Parsed and serialized as `<h1>` to + // `<h6>` elements. + heading: { + attrs: { level: { default: 1 } }, + content: "inline*", + group: "block", + defining: true, + parseDOM: [{ tag: "h1", attrs: { level: 1 } }, + { tag: "h2", attrs: { level: 2 } }, + { tag: "h3", attrs: { level: 3 } }, + { tag: "h4", attrs: { level: 4 } }, + { tag: "h5", attrs: { level: 5 } }, + { tag: "h6", attrs: { level: 6 } }], + toDOM(node: any) { return ["h" + node.attrs.level, 0] } + }, + + // :: NodeSpec A code listing. Disallows marks or non-text inline + // nodes by default. Represented as a `<pre>` element with a + // `<code>` element inside of it. + code_block: { + content: "text*", + marks: "", + group: "block", + code: true, + defining: true, + parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], + toDOM() { return preDOM } + }, + + // :: NodeSpec The text node. + text: { + group: "inline" + }, + + // :: NodeSpec An inline image (`<img>`) node. Supports `src`, + // `alt`, and `href` attributes. The latter two default to the empty + // string. + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "img[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt") + } + } + }], + toDOM(node: any) { return ["img", node.attrs] } + }, + + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM() { return brDOM } + }, + + ordered_list: { + ...orderedList, + content: 'list_item+', + group: 'block' + }, + bullet_list: { + content: 'list_item+', + group: 'block', + parseDOM: [{ tag: "ul" }, { style: "list-style-type=disc;" }], + toDOM() { return ulDOM } + }, + list_item: { + ...listItem, + content: 'paragraph block*' + } +} + +const emDOM: DOMOutputSpecArray = ["em", 0]; +const strongDOM: DOMOutputSpecArray = ["strong", 0]; +const codeDOM: DOMOutputSpecArray = ["code", 0]; +const underlineDOM: DOMOutputSpecArray = ["underline", 0]; + +// :: Object [Specs](#model.MarkSpec) for the marks in the schema. +export const marks: { [index: string]: MarkSpec } = { + // :: MarkSpec A link. Has `href` and `title` attributes. `title` + // defaults to the empty string. Rendered and parsed as an `<a>` + // element. + link: { + attrs: { + href: {}, + title: { default: null } + }, + inclusive: false, + parseDOM: [{ + tag: "a[href]", getAttrs(dom: any) { + return { href: dom.getAttribute("href"), title: dom.getAttribute("title") } + } + }], + toDOM(node: any) { return ["a", node.attrs, 0] } + }, + + // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. + // Has parse rules that also match `<i>` and `font-style: italic`. + em: { + parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], + toDOM() { return emDOM } + }, + + // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules + // also match `<b>` and `font-weight: bold`. + strong: { + parseDOM: [{ tag: "strong" }, + { tag: "b" }, + { style: "font-weight" }], + toDOM() { return strongDOM } + }, + + underline: { + parseDOM: [ + { tag: 'u' }, + { style: 'text-decoration=underline' } + ], + toDOM: () => ['span', { + style: 'text-decoration:underline' + }] + }, + + strikethrough: { + parseDOM: [ + { tag: 'strike' }, + { style: 'text-decoration=line-through' }, + { style: 'text-decoration-line=line-through' } + ], + toDOM: () => ['span', { + style: 'text-decoration-line:line-through' + }] + }, + + subscript: { + excludes: 'superscript', + parseDOM: [ + { tag: 'sub' }, + { style: 'vertical-align=sub' } + ], + toDOM: () => ['sub'] + }, + + superscript: { + excludes: 'subscript', + parseDOM: [ + { tag: 'sup' }, + { style: 'vertical-align=super' } + ], + toDOM: () => ['sup'] + }, + + + // :: MarkSpec Code font mark. Represented as a `<code>` element. + code: { + parseDOM: [{ tag: "code" }], + toDOM() { return codeDOM } + } +} + +// :: Schema +// This schema rougly corresponds to the document schema used by +// [CommonMark](http://commonmark.org/), minus the list elements, +// which are defined in the [`prosemirror-schema-list`](#schema-list) +// module. +// +// To reuse elements from this schema, extend or read from its +// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). +export const schema = new Schema({ nodes, marks })
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss new file mode 100644 index 000000000..fa43f5326 --- /dev/null +++ b/src/client/util/TooltipTextMenu.scss @@ -0,0 +1,54 @@ + +.tooltipMenu { + position: absolute; + z-index: 20; + background: rgb(19, 18, 18); + border: 1px solid silver; + border-radius: 4px; + padding: 2px 10px; + margin-bottom: 7px; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.tooltipMenu:before { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -6px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: silver; + } + .tooltipMenu:after { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -4.5px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: black; + } + + .menuicon { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + //color: rgb(19, 18, 18); + color: white; + line-height: 1; + padding: 0px 2px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 10px; + } + .strong, .heading { font-weight: bold; } + .em { font-style: italic; } + .underline {text-decoration: underline} + .superscript {vertical-align:super} + .subscript { vertical-align:sub } + .strikethrough {text-decoration-line:line-through}
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx new file mode 100644 index 000000000..3b87fe9de --- /dev/null +++ b/src/client/util/TooltipTextMenu.tsx @@ -0,0 +1,125 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +const { exampleSetup } = require("prosemirror-example-setup") +import { EditorState, Transaction, } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "./RichTextSchema"; +import React = require("react") +import "./TooltipTextMenu.scss"; +const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +import { library } from '@fortawesome/fontawesome-svg-core' +import { wrapInList, bulletList } from 'prosemirror-schema-list' +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; + + + +export class TooltipTextMenu { + + private tooltip: HTMLElement; + + constructor(view: EditorView) { + this.tooltip = document.createElement("div"); + this.tooltip.className = "tooltipMenu"; + + //add the div which is the tooltip + view.dom.parentNode!.appendChild(this.tooltip); + + //add additional icons + library.add(faListUl); + + //add the buttons to the tooltip + let items = [ + { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, + { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, + { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, + { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, + { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, + { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, + { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") } + ] + items.forEach(({ dom }) => this.tooltip.appendChild(dom)); + + //pointer down handler to activate button effects + this.tooltip.addEventListener("pointerdown", e => { + e.preventDefault(); + view.focus(); + items.forEach(({ command, dom }) => { + if (dom.contains(e.srcElement)) { + command(view.state, view.dispatch, view) + } + }) + }) + + this.update(view, undefined); + } + + // Helper function to create menu icons + icon(text: string, name: string) { + let span = document.createElement("span"); + span.className = "menuicon " + name; + span.title = name; + span.textContent = text; + return span; + } + + blockActive(view: EditorView) { + const { $from, to } = view.state.selection + + return to <= $from.end() && $from.parent.hasMarkup(schema.nodes.bulletList); + } + + //this doesn't currently work but hopefully will soon + unorderedListIcon(): HTMLSpanElement { + let span = document.createElement("span"); + let icon = document.createElement("FontAwesomeIcon"); + icon.className = "menuicon fa fa-smile-o"; + span.appendChild(icon); + return span; + } + + // Create an icon for a heading at the given level + heading(level: number) { + return { + command: setBlockType(schema.nodes.heading, { level }), + dom: this.icon("H" + level, "heading") + } + } + + //updates the tooltip menu when the selection changes + update(view: EditorView, lastState: EditorState | undefined) { + let state = view.state + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return + + // Hide the tooltip if the selection is empty + if (state.selection.empty) { + this.tooltip.style.display = "none" + return + } + + // Otherwise, reposition it and update its content + this.tooltip.style.display = "" + let { from, to } = state.selection + // These are in screen coordinates + //check this - tranform + let start = view.coordsAtPos(from), end = view.coordsAtPos(to) + // The box in which the tooltip is positioned, to use as base + let box = this.tooltip.offsetParent!.getBoundingClientRect() + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3) + this.tooltip.style.left = (left - box.left) + "px" + let width = Math.abs(start.left - end.left) / 2; + let mid = Math.min(start.left, end.left) + width; + //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 + this.tooltip.style.width = 120 + "px"; + this.tooltip.style.bottom = (box.bottom - start.top) + "px"; + } + + destroy() { this.tooltip.remove() } +}
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss new file mode 100644 index 000000000..f654b194b --- /dev/null +++ b/src/client/views/InkingCanvas.scss @@ -0,0 +1,32 @@ +.inking-canvas { + position: fixed; + top: -50000px; + left: -50000px; // z-index: 99; //overlays ink on top of everything + svg { + width: 100000px; + height: 100000px; + .highlight { + mix-blend-mode: multiply; + } + } +} + +.inking-control { + position: absolute; + right: 0; + bottom: 75px; + text-align: right; + .ink-panel { + margin-top: 12px; + &:first { + margin-top: 0; + } + } + .ink-size { + display: flex; + justify-content: space-between; + input { + width: 85%; + } + } +}
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx new file mode 100644 index 000000000..baf1567b7 --- /dev/null +++ b/src/client/views/InkingCanvas.tsx @@ -0,0 +1,161 @@ +import { observer } from "mobx-react"; +import { action } from "mobx"; +import { InkingControl } from "./InkingControl"; +import React = require("react"); +import { Transform } from "../util/Transform"; +import { Document } from "../../fields/Document"; +import { KeyStore } from "../../fields/KeyStore"; +import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField"; +import { JsxArgs } from "./nodes/DocumentView"; +import { InkingStroke } from "./InkingStroke"; +import "./InkingCanvas.scss" +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { Utils } from "../../Utils"; + + +interface InkCanvasProps { + getScreenTransform: () => Transform; + Document: Document; +} + +@observer +export class InkingCanvas extends React.Component<InkCanvasProps> { + + private _isDrawing: boolean = false; + private _idGenerator: string = ""; + + constructor(props: Readonly<InkCanvasProps>) { + super(props); + } + + get inkData(): StrokeMap { + return new Map(this.props.Document.GetData(KeyStore.Ink, InkField, new Map)); + } + + set inkData(value: StrokeMap) { + this.props.Document.SetData(KeyStore.Ink, value, InkField); + } + + componentDidMount() { + document.addEventListener("mouseup", this.handleMouseUp); + } + + componentWillUnmount() { + document.removeEventListener("mouseup", this.handleMouseUp); + } + + + @action + handleMouseDown = (e: React.PointerEvent): void => { + if (e.button != 0 || + InkingControl.Instance.selectedTool === InkTool.None) { + return; + } + e.stopPropagation() + if (InkingControl.Instance.selectedTool === InkTool.Eraser) { + return + } + e.stopPropagation() + const point = this.relativeCoordinatesForEvent(e); + + // start the new line, saves a uuid to represent the field of the stroke + this._idGenerator = Utils.GenerateGuid(); + let data = this.inkData; + data.set(this._idGenerator, + { + pathData: [point], + color: InkingControl.Instance.selectedColor, + width: InkingControl.Instance.selectedWidth, + tool: InkingControl.Instance.selectedTool + }); + this.inkData = data; + this._isDrawing = true; + } + + @action + handleMouseMove = (e: React.PointerEvent): void => { + if (!this._isDrawing || + InkingControl.Instance.selectedTool === InkTool.None) { + return; + } + e.stopPropagation() + if (InkingControl.Instance.selectedTool === InkTool.Eraser) { + return + } + const point = this.relativeCoordinatesForEvent(e); + + // add points to new line as it is being drawn + let data = this.inkData; + let strokeData = data.get(this._idGenerator); + if (strokeData) { + strokeData.pathData.push(point); + data.set(this._idGenerator, strokeData); + } + + this.inkData = data; + } + + @action + handleMouseUp = (e: MouseEvent): void => { + this._isDrawing = false; + } + + relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => { + let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY); + x += 50000 + y += 50000 + return { x, y }; + } + + @action + removeLine = (id: string): void => { + let data = this.inkData; + data.delete(id); + this.inkData = data; + } + + render() { + // styling for cursor + let canvasStyle = {}; + if (InkingControl.Instance.selectedTool === InkTool.None) { + canvasStyle = { pointerEvents: "none" }; + } else { + canvasStyle = { pointerEvents: "auto", cursor: "crosshair" }; + } + + // get data from server + // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField); + // if (!inkField || inkField == "<Waiting>") { + // return (<div className="inking-canvas" style={canvasStyle} + // onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} > + // <svg> + // </svg> + // </div >) + // } + + let lines = this.inkData; + + // parse data from server + let paths: Array<JSX.Element> = [] + Array.from(lines).map(item => { + let id = item[0]; + let strokeData = item[1]; + paths.push(<InkingStroke key={id} id={id} + line={strokeData.pathData} + color={strokeData.color} + width={strokeData.width} + tool={strokeData.tool} + deleteCallback={this.removeLine} />) + }) + + return ( + + <div className="inking-canvas" style={canvasStyle} + onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} > + <svg> + {paths} + </svg> + </div > + ) + } +}
\ No newline at end of file diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx new file mode 100644 index 000000000..929fb42a1 --- /dev/null +++ b/src/client/views/InkingControl.tsx @@ -0,0 +1,77 @@ +import { observable, action, computed } from "mobx"; +import { CirclePicker, ColorResult } from 'react-color' +import React = require("react"); +import "./InkingCanvas.scss" +import { InkTool } from "../../fields/InkField"; +import { observer } from "mobx-react"; + +@observer +export class InkingControl extends React.Component { + static Instance: InkingControl = new InkingControl({}); + @observable private _selectedTool: InkTool = InkTool.None; + @observable private _selectedColor: string = "#f44336"; + @observable private _selectedWidth: string = "25"; + + constructor(props: Readonly<{}>) { + super(props); + InkingControl.Instance = this + } + + @action + switchTool = (tool: InkTool): void => { + this._selectedTool = tool; + } + + @action + switchColor = (color: ColorResult): void => { + this._selectedColor = color.hex; + } + + @action + switchWidth = (width: string): void => { + this._selectedWidth = width; + } + + @computed + get selectedTool() { + return this._selectedTool; + } + + @computed + get selectedColor() { + return this._selectedColor; + } + + @computed + get selectedWidth() { + return this._selectedWidth; + } + + selected = (tool: InkTool) => { + if (this._selectedTool === tool) { + return { backgroundColor: "black", color: "white" } + } + return {} + } + + render() { + return ( + <div className="inking-control"> + <div className="ink-tools ink-panel"> + <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button> + <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button> + <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button> + <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button> + </div> + <div className="ink-size ink-panel"> + <label htmlFor="stroke-width">Size</label> + <input type="range" min="1" max="100" defaultValue="25" name="stroke-width" + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> + </div> + <div className="ink-color ink-panel"> + <CirclePicker onChange={this.switchColor} /> + </div> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx new file mode 100644 index 000000000..d724421d3 --- /dev/null +++ b/src/client/views/InkingStroke.tsx @@ -0,0 +1,66 @@ +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import { InkingControl } from "./InkingControl"; +import { InkTool } from "../../fields/InkField"; +import React = require("react"); + + +interface StrokeProps { + id: string; + line: Array<{ x: number, y: number }>; + color: string; + width: string; + tool: InkTool; + deleteCallback: (index: string) => void; +} + +@observer +export class InkingStroke extends React.Component<StrokeProps> { + + @observable private _strokeTool: InkTool = this.props.tool; + @observable private _strokeColor: string = this.props.color; + @observable private _strokeWidth: string = this.props.width; + + private _canvasColor: string = "#cdcdcd"; + + deleteStroke = (e: React.MouseEvent): void => { + if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) { + this.props.deleteCallback(this.props.id); + } + } + + parseData = (line: Array<{ x: number, y: number }>): string => { + if (line.length === 0) { + return ""; + } + const pathData = "M " + + line.map(p => { + return p.x + " " + p.y; + }).join(" L "); + return pathData; + } + + createStyle() { + switch (this._strokeTool) { + // add more tool styles here + default: + return { + fill: "none", + stroke: this._strokeColor, + strokeWidth: this._strokeWidth + "px", + } + } + } + + + render() { + let pathStyle = this.createStyle(); + let pathData = this.parseData(this.props.line); + + return ( + <path className={(this._strokeTool === InkTool.Highlighter) ? "highlight" : ""} + d={pathData} style={pathStyle} strokeLinejoin="round" strokeLinecap="round" + onMouseOver={this.deleteStroke} onMouseDown={this.deleteStroke} /> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index abacb258e..c9bdc24c2 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -17,6 +17,7 @@ import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { DocumentView } from './nodes/DocumentView'; import "./Main.scss"; +import { InkingControl } from './InkingControl'; configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action @@ -101,6 +102,7 @@ Documents.initProtos(mainDocId, (res?: Document) => { <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + <InkingControl /> </div>), document.getElementById('root')); }) diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index be5784b32..86d9a10e3 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -22,6 +22,9 @@ import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; import { Documents } from "../../documents/Documents"; +import { InkingCanvas } from "../InkingCanvas"; +import { InkingControl } from "../InkingControl"; +import { InkTool } from "../../../fields/InkField"; import React = require("react"); const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @@ -138,7 +141,7 @@ export class CollectionFreeFormView extends CollectionViewBase { let localTransform = this.getLocalTransform() localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) - console.log(localTransform) + // console.log(localTransform) this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); @@ -306,6 +309,7 @@ export class CollectionFreeFormView extends CollectionViewBase { style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }} ref={this._canvasRef}> {this.backgroundView} + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> {cursor} {this.views} </div> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 8e2ebd690..ab913897b 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,7 +1,7 @@ .documentView-node { position: absolute; background: #cdcdcd; - overflow: hidden; + //overflow: hidden; &.minimized { width: 30px; height: 30px; diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 3e21f34de..ab5849f09 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -21,4 +21,18 @@ overflow-x: hidden; color: initial; height: 100%; -}
\ No newline at end of file +} + +.menuicon { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + color: #888; + line-height: 1; + padding: 0 7px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 1.4em; + } + .strong, .heading { font-weight: bold; } + .em { font-style: italic; }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 04eb2052d..a6cee9957 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -2,7 +2,7 @@ import { action, IReactionDisposer, reaction } from "mobx"; import { baseKeymap } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { schema } from "prosemirror-schema-basic"; +import { schema } from "../../util/RichTextSchema"; import { EditorState, Transaction, } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Opt, FieldWaiting } from "../../../fields/Field"; @@ -10,6 +10,9 @@ import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; +import { Plugin } from 'prosemirror-state' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { TooltipTextMenu } from "../../util/TooltipTextMenu" import { ContextMenu } from "../../views/ContextMenu"; @@ -61,6 +64,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { history(), keymap({ "Mod-z": undo, "Mod-y": redo }), keymap(baseKeymap), + this.tooltipMenuPlugin() ] }; @@ -139,6 +143,14 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { e.stopPropagation(); } + tooltipMenuPlugin() { + return new Plugin({ + view(_editorView) { + return new TooltipTextMenu(_editorView) + } + }) + } + render() { return (<div className="formattedTextBox-cont" onPointerDown={this.onPointerDown} diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts new file mode 100644 index 000000000..a475e2aae --- /dev/null +++ b/src/fields/InkField.ts @@ -0,0 +1,47 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; + +export enum InkTool { + None, + Pen, + Highlighter, + Eraser +} +export interface StrokeData { + pathData: Array<{ x: number, y: number }>; + color: string; + width: string; + tool: InkTool; +} +export type StrokeMap = Map<string, StrokeData>; + +export class InkField extends BasicField<StrokeMap> { + constructor(data: StrokeMap = new Map, id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new InkField("${this.Data}")`; + } + + Copy() { + return new InkField(this.Data); + } + + ToJson(): { _id: string; type: Types; data: any; } { + return { + type: Types.Ink, + data: this.Data, + _id: this.Id, + } + } + + static FromJson(id: string, data: any): InkField { + let map = new Map<string, StrokeData>(); + Object.keys(data).forEach(key => { + map.set(key, data[key]); + }); + return new InkField(map, id, false); + } +}
\ No newline at end of file diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index 1327bd9f4..259d1acaf 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -27,4 +27,5 @@ export namespace KeyStore { export const ActiveFrame = new Key("ActiveFrame"); export const DocumentText = new Key("DocumentText"); export const Thumbnail = new Key("Thumbnail"); + export const Ink = new Key("Ink"); } diff --git a/src/server/Message.ts b/src/server/Message.ts index 340e9b34a..5e97a5edf 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -45,7 +45,7 @@ export class GetFieldArgs { } export enum Types { - Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, PDF + Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Ink, PDF } export class DocumentTransfer implements Transferable { diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 98ec99372..3b9d14891 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -1,17 +1,17 @@ -import { Field } from './../fields/Field'; -import { TextField } from './../fields/TextField'; -import { NumberField } from './../fields/NumberField'; -import { RichTextField } from './../fields/RichTextField'; -import { Key } from './../fields/Key'; -import { ImageField } from './../fields/ImageField'; -import { ListField } from './../fields/ListField'; -import { Document } from './../fields/Document'; -import { Server } from './../client/Server'; -import { Types } from './Message'; -import { Utils } from '../Utils'; -import { HtmlField } from '../fields/HtmlField'; -import { WebField } from '../fields/WebField'; -import { PDFField } from '../fields/PDFField'; +import {HtmlField} from '../fields/HtmlField'; +import {InkField} from '../fields/InkField'; +import {PDFField} from '../fields/PDFField'; +import {WebField} from '../fields/WebField'; +import {Utils} from '../Utils'; +import {Document} from './../fields/Document'; +import {Field} from './../fields/Field'; +import {ImageField} from './../fields/ImageField'; +import {Key} from './../fields/Key'; +import {ListField} from './../fields/ListField'; +import {NumberField} from './../fields/NumberField'; +import {RichTextField} from './../fields/RichTextField'; +import {TextField} from './../fields/TextField'; +import {Types} from './Message'; export class ServerUtils { public static FromJson(json: any): Field { @@ -44,6 +44,8 @@ export class ServerUtils { return new PDFField(new URL(data), id, false) case Types.List: return ListField.FromJson(id, data) + case Types.Ink: + return InkField.FromJson(id, data); case Types.Document: let doc: Document = new Document(id, false) let fields: [string, string][] = data as [string, string][] |