diff options
author | Hannah Chow <hannah_chow@brown.edu> | 2019-03-10 22:47:11 -0400 |
---|---|---|
committer | Hannah Chow <hannah_chow@brown.edu> | 2019-03-10 22:47:11 -0400 |
commit | ce0d749c291609b7acca6db490bfcc131cf3996d (patch) | |
tree | b7c6e6c1dbfb1424790d496d6f1e1520bfac7798 /src | |
parent | 018d92a8a49e7e1a3a41ae87e24a9a8ba60619d6 (diff) | |
parent | 845a7989a6ff66b1c922d1f7f69c7560b6783dd8 (diff) |
mergging
Diffstat (limited to 'src')
42 files changed, 2371 insertions, 126 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/documents/Documents.ts b/src/client/documents/Documents.ts index 1d24ff7d2..b78762018 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -15,6 +15,14 @@ import { Key } from "../../fields/Key" import { Field } from "../../fields/Field"; import { KeyValueBox } from "../views/nodes/KeyValueBox" import { KVPField } from "../../fields/KVPField"; +import { VideoField } from "../../fields/VideoField" +import { VideoBox } from "../views/nodes/VideoBox"; +import { AudioField } from "../../fields/AudioField"; +import { AudioBox } from "../views/nodes/AudioBox"; +import { PDFField } from "../../fields/PDFField"; +import { PDFBox } from "../views/nodes/PDFBox"; +import { CollectionPDFView } from "../views/collections/CollectionPDFView"; +import { RichTextField } from "../../fields/RichTextField"; export interface DocumentOptions { x?: number; @@ -38,11 +46,17 @@ export namespace Documents { let webProto: Document; let collProto: Document; let kvpProto: Document; + let videoProto: Document; + let audioProto: Document; + let pdfProto: Document; const textProtoId = "textProto"; + const pdfProtoId = "pdfProto"; const imageProtoId = "imageProto"; const webProtoId = "webProto"; const collProtoId = "collectionProto"; const kvpProtoId = "kvpProto"; + const videoProtoId = "videoProto" + const audioProtoId = "audioProto"; export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { @@ -92,6 +106,15 @@ export namespace Documents { textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); } + function GetPdfPrototype(): Document { + if (!pdfProto) { + pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("AnnotationsKey"), + { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] }); + pdfProto.SetNumber(KeyStore.CurPage, 1); + pdfProto.SetText(KeyStore.BackgroundLayout, PDFBox.LayoutString()); + } + return pdfProto; + } function GetWebPrototype(): Document { return webProto ? webProto : webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), @@ -108,6 +131,17 @@ export namespace Documents { kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) } + function GetVideoPrototype(): Document { + return videoProto ? videoProto : + videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", VideoBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) + } + function GetAudioPrototype(): Document { + return audioProto ? audioProto : + audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) + } + export function ImageDocument(url: string, options: DocumentOptions = {}) { let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, @@ -117,8 +151,27 @@ export namespace Documents { doc.SetText(KeyStore.OverlayLayout, FixedCaption()); return doc; } + export function VideoDocument(url: string, options: DocumentOptions = {}) { + let doc = SetInstanceOptions(GetVideoPrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, + new URL(url), VideoField); + doc.SetText(KeyStore.Caption, "my caption..."); + doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); + doc.SetText(KeyStore.OverlayLayout, FixedCaption()); + return doc; + } + export function AudioDocument(url: string, options: DocumentOptions = {}) { + let doc = SetInstanceOptions(GetAudioPrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, + new URL(url), AudioField); + doc.SetText(KeyStore.Caption, "my caption..."); + doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); + doc.SetText(KeyStore.OverlayLayout, FixedCaption()); + return doc; + } export function TextDocument(options: DocumentOptions = {}) { - return SetInstanceOptions(GetTextPrototype(), options, "", TextField); + return SetInstanceOptions(GetTextPrototype(), options, "", RichTextField); + } + export function PdfDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetPdfPrototype(), options, new URL(url), PDFField); } export function WebDocument(url: string, options: DocumentOptions = {}) { return SetInstanceOptions(GetWebPrototype(), options, new URL(url), WebField); @@ -141,8 +194,6 @@ export namespace Documents { return assignOptions(deleg, options); } - - // example of custom display string for an image that shows a caption. function EmbeddedCaption() { return `<div style="height:100%"> diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 95bf44560..bb9a3b793 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -3,6 +3,8 @@ import { CollectionDockingView } from "../views/collections/CollectionDockingVie import { Document } from "../../fields/Document" import { action } from "mobx"; import { DocumentView } from "../views/nodes/DocumentView"; +import { ImageField } from "../../fields/ImageField"; +import { KeyStore } from "../../fields/KeyStore"; export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document) { let onRowMove = action((e: PointerEvent): void => { @@ -104,6 +106,7 @@ export namespace DragManager { const scaleX = rect.width / w, scaleY = rect.height / h; let x = rect.left, y = rect.top; // const offsetX = e.x - rect.left, offsetY = e.y - rect.top; + let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; dragElement.style.position = "absolute"; @@ -114,10 +117,23 @@ export namespace DragManager { dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; dragElement.style.width = `${rect.width / scaleX}px`; dragElement.style.height = `${rect.height / scaleY}px`; - // It seems like the above code should be able to just be this: - // dragElement.style.transform = `translate(${x}px, ${y}px)`; - // dragElement.style.width = `${rect.width}px`; - // dragElement.style.height = `${rect.height}px`; + + // bcz: PDFs don't show up if you clone them because they contain a canvas. + // however, PDF's have a thumbnail field that contains an image of their canvas. + // So we replace the pdf's canvas with the image thumbnail + const docView: DocumentView = dragData["documentView"]; + const doc: Document = docView ? docView.props.Document : dragData["document"]; + var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; + let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField); + if (pdfBox && pdfBox.childElementCount && thumbnail) { + let img = new Image(); + img!.src = thumbnail.toString(); + img!.style.position = "absolute"; + img!.style.width = `${rect.width / scaleX}px`; + img!.style.height = `${rect.height / scaleY}px`; + pdfBox.replaceChild(img!, pdfBox.children[0]) + } + dragDiv.appendChild(dragElement); let hideSource = false; @@ -139,8 +155,6 @@ export namespace DragManager { y += e.movementY; if (e.shiftKey) { abortDrag(); - const docView: DocumentView = dragData["documentView"]; - const doc: Document = docView ? docView.props.Document : dragData["document"]; CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); } dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; 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/Scripting.ts b/src/client/util/Scripting.ts index 44f36fe19..46bd1a206 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,5 +1,5 @@ -import * as ts from "typescript" -// let ts = (window as any).ts; +// import * as ts from "typescript" +let ts = (window as any).ts; import { Opt, Field } from "../../fields/Field"; import { Document } from "../../fields/Document"; import { NumberField } from "../../fields/NumberField"; 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..0d87c1239 --- /dev/null +++ b/src/client/views/InkingCanvas.tsx @@ -0,0 +1,171 @@ +import { observer } from "mobx-react"; +import { action, computed } 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"; +import { FieldWaiting } from "../../fields/Field"; +import { getMapLikeKeys } from "mobx/lib/internal"; + + +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); + } + + @computed + get inkData(): StrokeMap { + let map = this.props.Document.GetT(KeyStore.Ink, InkField); + if (!map || map === FieldWaiting) { + return new Map; + } + return new Map(map.Data); + } + + 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, + page: this.props.Document.GetNumber(KeyStore.CurPage, 0) + }); + 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> = [] + let curPage = this.props.Document.GetNumber(KeyStore.CurPage, 0) + Array.from(lines).map(item => { + let id = item[0]; + let strokeData = item[1]; + if (strokeData.page == 0 || strokeData.page == curPage) + 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 d845fa7a3..6f08566bc 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 @@ -51,22 +52,30 @@ Documents.initProtos(mainDocId, (res?: Document) => { } let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" let weburl = "https://cs.brown.edu/courses/cs166/"; + let audiourl = "http://techslides.com/demos/samples/sample.mp3"; + let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addVideoNode = action(() => Documents.VideoDocument(videourl, {width: 200, height:200, title: "video node"})); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - + let addAudioNode = action(() => Documents.AudioDocument(audiourl,{ width: 200, height: 200, title: "audio node" } )) let addClick = (creator: () => Document) => action(() => mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) ); let imgRef = React.createRef<HTMLDivElement>(); + let pdfRef = React.createRef<HTMLDivElement>(); let webRef = React.createRef<HTMLDivElement>(); let textRef = React.createRef<HTMLDivElement>(); let schemaRef = React.createRef<HTMLDivElement>(); + let videoRef = React.createRef<HTMLDivElement>(); + let audioRef = React.createRef<HTMLDivElement>(); let colRef = React.createRef<HTMLDivElement>(); ReactDOM.render(( @@ -94,8 +103,15 @@ Documents.initProtos(mainDocId, (res?: Document) => { <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> <div className="main-buttonDiv" style={{ bottom: '125px' }} > <button onClick={clearDatabase}>Clear Database</button></div> + <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}> + <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div> + <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}> + <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div> + <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> + <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/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index c51fba908..f01c538e6 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -35,6 +35,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp private _goldenLayout: any = null; private _containerRef = React.createRef<HTMLDivElement>(); private _fullScreen: any = null; + private _flush: boolean = false; constructor(props: SubCollectionViewProps) { super(props); @@ -164,7 +165,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); } - _flush: boolean = false; @action onPointerUp = (e: React.PointerEvent): void => { if (this._flush) { diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index f432e8cc3..b059163ed 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,8 +1,9 @@ .collectionfreeformview-container { - .collectionfreeformview > .jsx-parser{ + .collectionfreeformview > .jsx-parser{ position:absolute; height: 100%; + width: 100%; } border-style: solid; @@ -18,7 +19,33 @@ top: 0; left: 0; width:100%; - height: 100% + height: 100%; + } +} +.collectionfreeformview-overlay { + + .collectionfreeformview > .jsx-parser{ + position:absolute; + height: 100%; + } + .formattedTextBox-cont { + background:yellow; + } + + border-style: solid; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + .collectionfreeformview { + position: absolute; + top: 0; + left: 0; + width:100%; + height: 100%; } } diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 7e81e1e4a..e12d3d15c 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,27 +1,31 @@ -import { observable, action, computed } from "mobx"; +import { action, computed, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { FieldWaiting } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; import { TextField } from "../../../fields/TextField"; +import { Documents } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { InkingCanvas } from "../InkingCanvas"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { KeyValueBox } from "../nodes/KeyValueBox"; +import { PDFBox } from "../nodes/PDFBox"; import { WebBox } from "../nodes/WebBox"; -import { KeyValueBox } from "../nodes/KeyValueBox" import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; -import { Documents } from "../../documents/Documents"; import React = require("react"); +import { render } from "pug"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer @@ -69,8 +73,8 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onPointerDown = (e: React.PointerEvent): void => { - if ((e.button === 2 && this.props.active()) || - !e.defaultPrevented) { + if (((e.button === 2 && this.props.active()) || + !e.defaultPrevented) && (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -140,7 +144,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); @@ -225,21 +229,24 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get views() { + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); if (lvalue && lvalue != FieldWaiting) { return lvalue.Data.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} - AddDocument={this.props.addDocument} - RemoveDocument={this.props.removeDocument} - ScreenToLocalTransform={this.getTransform} - isTopMost={false} - SelectOnLoad={doc.Id === this._selectOnLoaded} - ContentScaling={this.noScaling} - PanelWidth={doc.Width} - PanelHeight={doc.Height} - ContainingCollectionView={this.props.CollectionView} - focus={this.focusDocument} - />); + var page = doc.GetNumber(KeyStore.Page, 0); + return (page != curPage && page != 0) ? (null) : + (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} + AddDocument={this.props.addDocument} + RemoveDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getTransform} + isTopMost={false} + SelectOnLoad={doc.Id === this._selectOnLoaded} + ContentScaling={this.noScaling} + PanelWidth={doc.Width} + PanelHeight={doc.Height} + ContainingCollectionView={this.props.CollectionView} + focus={this.focusDocument} + />); }) } return null; @@ -249,7 +256,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get backgroundView() { return !this.backgroundLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} bindings={this.props.bindings} jsx={this.backgroundLayout} showWarnings={true} @@ -260,7 +267,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get overlayView() { return !this.overlayLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} bindings={this.props.bindings} jsx={this.overlayLayout} showWarnings={true} @@ -279,7 +286,6 @@ export class CollectionFreeFormView extends CollectionViewBase { } render() { - //determines whether preview text cursor should be visible (ie when user taps this collection it should) let cursor = null; if (this._previewCursorVisible) { @@ -294,7 +300,7 @@ export class CollectionFreeFormView extends CollectionViewBase { const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); return ( - <div className="collectionfreeformview-container" + <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} onPointerDown={this.onPointerDown} onKeyPress={this.onKeyDown} onWheel={this.onPointerWheel} @@ -308,6 +314,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> @@ -315,4 +322,4 @@ export class CollectionFreeFormView extends CollectionViewBase { </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx new file mode 100644 index 000000000..7fd9f0f11 --- /dev/null +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -0,0 +1,55 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionView, CollectionViewType } from "./CollectionView"; +import { CollectionViewProps } from "./CollectionViewBase"; +import React = require("react"); + + +@observer +export class CollectionPDFView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<${CollectionPDFView.name} Document={Document} + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; + } + + @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0; + @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0; + + @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); } + @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } + @computed private get uIButtons() { + return ( + <div className="pdfBox-buttonTray" key="tray"> + <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button> + <button className="pdfButton" onClick={this.onPageForward}>{">"}</button> + </div>); + } + + // "inherited" CollectionView API starts here... + + public active: () => boolean = () => CollectionView.Active(this); + + addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } + removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } + + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { } }); + } + } + + get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; } + get subView(): any { return CollectionView.SubView(this); } + + render() { + return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> + {this.subView} + {this.props.isSelected() ? this.uIButtons : (null)} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 31824763d..f154909bb 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { action } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { ListField } from "../../../fields/ListField"; @@ -28,32 +28,39 @@ export const COLLECTION_BORDER_WIDTH = 2; export class CollectionView extends React.Component<CollectionViewProps> { public static LayoutString(fieldKey: string = "DataKey") { - return `<CollectionView Document={Document} + return `<${CollectionView.name} Document={Document} ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; } - public active = () => { - var isSelected = this.props.isSelected(); - var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); - var topMost = this.props.isTopMost; + + public active: () => boolean = () => CollectionView.Active(this); + addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } + removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } + get subView() { return CollectionView.SubView(this); } + + public static Active(self: CollectionView): boolean { + var isSelected = self.props.isSelected(); + var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == self); + var topMost = self.props.isTopMost; return isSelected || childSelected || topMost; } + @action - addDocument = (doc: Document): void => { - if (this.props.Document.Get(this.props.fieldKey) instanceof Field) { + public static AddDocument(props: CollectionViewProps, doc: Document) { + doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0)); + if (props.Document.Get(props.fieldKey) instanceof Field) { //TODO This won't create the field if it doesn't already exist - const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) value.push(doc); } else { - this.props.Document.SetData(this.props.fieldKey, [doc], ListField); + props.Document.SetData(props.fieldKey, [doc], ListField); } } - @action - removeDocument = (doc: Document): boolean => { + public static RemoveDocument(props: CollectionViewProps, doc: Document): boolean { //TODO This won't create the field if it doesn't already exist - const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>()) + const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) let index = -1; for (let i = 0; i < value.length; i++) { if (value[i].Id == doc.Id) { @@ -84,13 +91,8 @@ export class CollectionView extends React.Component<CollectionViewProps> { } } - set collectionViewType(type: CollectionViewType) { - let Document = this.props.Document; - Document.SetData(KeyStore.ViewType, type, NumberField); - } - specificContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) @@ -98,36 +100,20 @@ export class CollectionView extends React.Component<CollectionViewProps> { } } - render() { - let viewType = this.collectionViewType; - let subView: JSX.Element; - switch (viewType) { - case CollectionViewType.Freeform: - subView = (<CollectionFreeFormView {...this.props} - addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />) - break; - case CollectionViewType.Schema: - subView = (<CollectionSchemaView {...this.props} - addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />) - break; - case CollectionViewType.Docking: - subView = (<CollectionDockingView {...this.props} - addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />) - break; - case CollectionViewType.Tree: - subView = (<CollectionTreeView {...this.props} - addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active} - CollectionView={this} />) - break; - default: - subView = <div></div> - break; + public static SubView(self: CollectionView) { + let subProps = { ...self.props, addDocument: self.addDocument, removeDocument: self.removeDocument, active: self.active, CollectionView: self } + switch (self.collectionViewType) { + case CollectionViewType.Freeform: return (<CollectionFreeFormView {...subProps} />) + case CollectionViewType.Schema: return (<CollectionSchemaView {...subProps} />) + case CollectionViewType.Docking: return (<CollectionDockingView {...subProps} />) + case CollectionViewType.Tree: return (<CollectionTreeView {...subProps} />) } - return (<div onContextMenu={this.specificContextMenu}> - {subView} + return (null); + } + + render() { + return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> + {this.subView} </div>) } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx new file mode 100644 index 000000000..a2c7be1a8 --- /dev/null +++ b/src/client/views/nodes/Annotation.tsx @@ -0,0 +1,117 @@ +import "./ImageBox.scss"; +import React = require("react") +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps{ + Span: HTMLSpanElement; + X: number; + Y: number; + Highlights: any[]; + Annotations: any[]; + CurrAnno: any[]; + +} + +/** + * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color + * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). + * Also need to support multiline highlighting + * + * Written by: Andrew Kim + */ +@observer +export class Annotation extends React.Component<IProps> { + + /** + * changes color of the span (highlighted section) + */ + onColorChange = (e:React.PointerEvent) => { + if (e.currentTarget.innerHTML == "r"){ + this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)" + } else if (e.currentTarget.innerHTML == "b"){ + this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)" + } else if (e.currentTarget.innerHTML == "y"){ + this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)" + } else if (e.currentTarget.innerHTML == "g"){ + this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)" + } + + } + + /** + * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this + */ + @action + onRemove = (e:any) => { + let index:number = -1; + //finding the highlight in the highlight array + this.props.Highlights.forEach((e) => { + for (let i = 0; i < e.spans.length; i++){ + if (e.spans[i] == this.props.Span){ + index = this.props.Highlights.indexOf(e); + this.props.Highlights.splice(index, 1); + } + } + }) + + //removing from CurrAnno and Annotation array + this.props.Annotations.splice(index, 1); + this.props.CurrAnno.pop() + + //removing span from div + if(this.props.Span.parentElement){ + let nodesArray = this.props.Span.parentElement.childNodes; + nodesArray.forEach((e) => { + if (e == this.props.Span){ + if (this.props.Span.parentElement){ + this.props.Highlights.forEach((item) => { + if (item == e){ + item.remove(); + } + }) + e.remove(); + } + } + }) + } + + + } + + render() { + return ( + <div + style = {{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + + }}> + <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}> + <button + style = {{borderRadius: "25px", width:"25%", height:"100%"}} + onClick = {this.onRemove} + >x</button> + <div style = {{width:"75%", height: "100%" , display:"inline-block"}}> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button> + </div> + + </div> + <div style = {{width:"200px", height:"200"}}> + <textarea style = {{width: "100%", height: "100%"}} + defaultValue = "Enter Text Here..." + + ></textarea> + </div> + </div> + + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss new file mode 100644 index 000000000..704cdc31c --- /dev/null +++ b/src/client/views/nodes/AudioBox.scss @@ -0,0 +1,4 @@ +.audiobox-cont{ + height: 100%; + width: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx new file mode 100644 index 000000000..f7d89843d --- /dev/null +++ b/src/client/views/nodes/AudioBox.tsx @@ -0,0 +1,44 @@ +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { ContextMenu } from "../../views/ContextMenu"; +import { observable, action } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; +import { AudioField } from "../../../fields/AudioField"; +import "./AudioBox.scss" +import { NumberField } from "../../../fields/NumberField"; + +@observer +export class AudioBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(AudioBox) } + + constructor(props: FieldViewProps) { + super(props); + } + + + + componentDidMount() { + } + + componentWillUnmount() { + } + + + render() { + let field = this.props.doc.Get(this.props.fieldKey) + let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3": + field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3"; + + return ( + <div> + <audio controls className = "audiobox-cont"> + <source src = {path} type="audio/mpeg"/> + Not supported. + </audio> + </div> + ) + } +}
\ No newline at end of file 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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d80abb460..6d903b955 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,12 +12,16 @@ import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { VideoBox } from "../nodes/VideoBox"; +import { AudioBox } from "../nodes/AudioBox"; import { Documents } from "../../documents/Documents" import { KeyValueBox } from "./KeyValueBox" import { WebBox } from "../nodes/WebBox"; +import { PDFBox } from "../nodes/PDFBox"; import "./DocumentView.scss"; import React = require("react"); import { TextField } from "../../../fields/TextField"; @@ -258,7 +262,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } @computed get mainContent() { return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }} bindings={this._documentBindings} jsx={this.layout} showWarnings={true} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 9e63006d1..49f4cefce 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -8,10 +8,15 @@ import { NumberField } from "../../../fields/NumberField"; import { RichTextField } from "../../../fields/RichTextField"; import { ImageField } from "../../../fields/ImageField"; import { WebField } from "../../../fields/WebField"; +import { VideoField } from "../../../fields/VideoField" import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { WebBox } from "./WebBox"; +import { VideoBox } from "./VideoBox"; +import { AudioBox } from "./AudioBox"; +import { AudioField } from "../../../fields/AudioField"; + // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -55,6 +60,12 @@ export class FieldView extends React.Component<FieldViewProps> { } else if (field instanceof WebField) { return <WebBox {...this.props} /> + } + else if (field instanceof VideoField){ + return <VideoBox {...this.props}/> + } + else if (field instanceof AudioField){ + return <AudioBox {...this.props}/> } // bcz: this belongs here, but it doesn't render well so taking it out for now // else if (field instanceof HtmlField) { diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 21bd43b6e..ab5849f09 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -11,10 +11,28 @@ .formattedTextBox-cont { background: white; padding: 1; - border: black; - border-width: 10; + border-width: 1px; + border-radius: 2px; + border-color:black; + box-sizing: border-box; + background: white; + border-style:solid; overflow-y: scroll; 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..ad7ddf37a 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,11 +64,12 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { history(), keymap({ "Mod-z": undo, "Mod-y": redo }), keymap(baseKeymap), + this.tooltipMenuPlugin() ] }; let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); - if (field && field != FieldWaiting) { + if (field && field != FieldWaiting && field.Data) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -139,8 +143,20 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { e.stopPropagation(); } + tooltipMenuPlugin() { + return new Plugin({ + view(_editorView) { + return new TooltipTextMenu(_editorView) + } + }) + } + + onKeyPress(e: React.KeyboardEvent) { + e.stopPropagation(); + } render() { return (<div className="formattedTextBox-cont" + onKeyPress={this.onKeyPress} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} onWheel={this.onPointerWheel} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 8c44395f4..30910fb1f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,15 +1,15 @@ +import { action, observable } from 'mobx'; +import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import "./ImageBox.scss"; -import React = require("react") -import { ImageField } from '../../../fields/ImageField'; -import { FieldViewProps, FieldView } from './FieldView'; import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { ContextMenu } from "../../views/ContextMenu"; -import { observable, action } from 'mobx'; +import { ImageField } from '../../../fields/ImageField'; import { KeyStore } from '../../../fields/KeyStore'; +import { ContextMenu } from "../../views/ContextMenu"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import React = require("react") @observer export class ImageBox extends React.Component<FieldViewProps> { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index e8ebd50be..ac8c949a9 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,37 +1,18 @@ -import { IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; -import { EditorView } from 'prosemirror-view'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Document } from '../../../fields/Document'; -import { Opt, FieldWaiting } from '../../../fields/Field'; +import { FieldWaiting } from '../../../fields/Field'; import { KeyStore } from '../../../fields/KeyStore'; import { FieldView, FieldViewProps } from './FieldView'; -import { KeyValuePair } from "./KeyValuePair"; import "./KeyValueBox.scss"; +import { KeyValuePair } from "./KeyValuePair"; import React = require("react") @observer export class KeyValueBox extends React.Component<FieldViewProps> { public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } - private _ref: React.RefObject<HTMLDivElement>; - private _editorView: Opt<EditorView>; - private _reactionDisposer: Opt<IReactionDisposer>; - - - constructor(props: FieldViewProps) { - super(props); - - this._ref = React.createRef(); - } - - - - shouldComponentUpdate() { - return false; - } - onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this.props.isSelected()) { diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss new file mode 100644 index 000000000..9f92410d4 --- /dev/null +++ b/src/client/views/nodes/PDFBox.scss @@ -0,0 +1,15 @@ +.react-pdf__Page { + transform-origin: left top; + position: absolute; +} +.react-pdf__Document { + position: absolute; +} +.pdfBox-buttonTray { + position:absolute; + z-index: 25; +} +.pdfBox-contentContainer { + position: absolute; + transform-origin: "left top"; +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx new file mode 100644 index 000000000..70a70c7c8 --- /dev/null +++ b/src/client/views/nodes/PDFBox.tsx @@ -0,0 +1,490 @@ +import * as htmlToImage from "html-to-image"; +import { action, computed, observable, reaction, IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import 'react-image-lightbox/style.css'; +import Measure from "react-measure"; +//@ts-ignore +import { Document, Page } from "react-pdf"; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import { FieldWaiting, Opt } from '../../../fields/Field'; +import { ImageField } from '../../../fields/ImageField'; +import { KeyStore } from '../../../fields/KeyStore'; +import { PDFField } from '../../../fields/PDFField'; +import { Utils } from '../../../Utils'; +import { Annotation } from './Annotation'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import "./PDFBox.scss"; +import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here +import React = require("react") + +/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx + * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, + * area selection (I call it stickies), embedded ink node for directly annotating using a pen or + * mouse, and pagination. + * + * + * HOW TO USE: + * AREA selection: + * 1) Click on Area button. + * 2) click on any part of the PDF, and drag to get desired sized area shape + * 3) You can write on the area (hence the reason why it's called sticky) + * 4) to make another area, you need to click on area button AGAIN. + * + * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...) + * 1) just click and drag on a text + * 2) click highlight + * 3) for annotation, just pull your cursor over to that text + * 4) another method: click on highlight first and then drag on your desired text + * 5) To make another highlight, you need to reclick on the button + * + * Draw: + * 1) click draw and select color. then just draw like there's no tomorrow. + * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. + * + * Pagination: + * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. + * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. + * + * + * written by: Andrew Kim + */ +@observer +export class PDFBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(PDFBox); } + + private _mainDiv = React.createRef<HTMLDivElement>() + private _pdf = React.createRef<HTMLCanvasElement>(); + + //very useful for keeping track of X and y position throughout the PDF Canvas + private initX: number = 0; + private initY: number = 0; + private initPage: boolean = false; + + //checks if tool is on + private _toolOn: boolean = false; //checks if tool is on + private _pdfContext: any = null; //gets pdf context + private bool: Boolean = false; //general boolean debounce + private currSpan: any;//keeps track of current span (for highlighting) + + private _currTool: any; //keeps track of current tool button reference + private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool + private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference + + private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference + private _currColor: string = "black"; //current color that user selected (for ink/pen) + + private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference + private _highlightToolOn: boolean = false; + private _pdfCanvas: any; + private _reactionDisposer: Opt<IReactionDisposer>; + + @observable private _perPageInfo: Object[] = []; //stores pageInfo + @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno + + @observable private _currAnno: any = [] + @observable private _interactive: boolean = false; + @observable private _loaded: boolean = false; + + @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); } + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.curPage, + () => { + if (this.curPage && this.initPage) { + this.saveThumbnail(); + this._interactive = true; + } else { + if (this.curPage) + this.initPage = true; + } + }, + { fireImmediately: true }); + + } + + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + /** + * selection tool used for area highlighting (stickies). Kinda temporary + */ + selectionTool = () => { + this._toolOn = true; + } + /** + * when user draws on the canvas. When mouse pointer is down + */ + drawDown = (e: PointerEvent) => { + this.initX = e.offsetX; + this.initY = e.offsetY; + this._pdfContext.beginPath(); + this._pdfContext.lineTo(this.initX, this.initY); + this._pdfContext.strokeStyle = this._currColor; + this._pdfCanvas.addEventListener("pointermove", this.drawMove); + this._pdfCanvas.addEventListener("pointerup", this.drawUp); + + } + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this._pdfContext.lineTo(x, y); + this._pdfContext.stroke(); + } + + drawUp = (e: PointerEvent) => { + this._pdfContext.closePath(); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + } + + + /** + * highlighting helper function + */ + makeEditableAndHighlight = (colour: string) => { + var range, sel = window.getSelection(); + if (sel.rangeCount && sel.getRangeAt) { + range = sel.getRangeAt(0); + } + document.designMode = "on"; + if (!document.execCommand("HiliteColor", false, colour)) { + document.execCommand("HiliteColor", false, colour); + } + + if (range) { + sel.removeAllRanges(); + sel.addRange(range); + + let obj: Object = { parentDivs: [], spans: [] }; + //@ts-ignore + if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case + obj = this.highlightNodes(range.commonAncestorContainer.childNodes) + } else { //single line highlighting case + let parentDiv = range.commonAncestorContainer.parentElement + if (parentDiv) { + if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten + obj = this.highlightNodes(parentDiv.childNodes) + } else { + parentDiv.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + obj.parentDivs.push(parentDiv) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + obj.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + } + } + } + this._pageInfo.divs.push(obj); + + } + document.designMode = "off"; + } + + highlightNodes = (nodes: NodeListOf<ChildNode>) => { + let temp = { parentDivs: [], spans: [] } + nodes.forEach((div) => { + div.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + temp.parentDivs.push(div) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + temp.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + + }) + return temp; + } + + /** + * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES + */ + @action + onEnter = (e: any) => { + let span: HTMLSpanElement = e.toElement; + let index: any; + this._pageInfo.divs.forEach((obj: any) => { + obj.spans.forEach((element: any) => { + if (element == span) { + if (!index) { + index = this._pageInfo.divs.indexOf(obj); + } + } + }) + }) + + if (this._pageInfo.anno.length >= index + 1) { + if (this._currAnno.length == 0) { + this._currAnno.push(this._pageInfo.anno[index]); + } + } else { + if (this._currAnno.length == 0) { //if there are no current annotation + let div = span.offsetParent; + //@ts-ignore + let divX = div.style.left + //@ts-ignore + let divY = div.style.top + //slicing "px" from the end + divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) + divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) + let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} /> + this._pageInfo.anno.push(annotation); + this._currAnno.push(annotation); + } + } + + } + + /** + * highlight function for highlighting actual text. This works fine. + */ + highlight = (color: string) => { + if (window.getSelection()) { + try { + if (!document.execCommand("hiliteColor", false, color)) { + this.makeEditableAndHighlight(color); + } + } catch (ex) { + this.makeEditableAndHighlight(color) + } + } + } + + /** + * controls the area highlighting (stickies) Kinda temporary + */ + onPointerDown = (e: React.PointerEvent) => { + if (this._toolOn) { + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + + } + } + + /** + * controls area highlighting and partially highlighting. Kinda temporary + */ + @action + onPointerUp = (e: React.PointerEvent) => { + if (this._highlightToolOn) { + this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. + this._highlightToolOn = false; + } + if (this._toolOn) { + let mouse = e.nativeEvent; + let finalX = mouse.offsetX; + let finalY = mouse.offsetY; + let width = Math.abs(finalX - this.initX); //width + let height = Math.abs(finalY - this.initY); //height + + //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky + if (finalX < this.initX) { + this.initX = finalX; + } + if (finalY < this.initY) { + this.initY = finalY; + } + + if (this._mainDiv.current) { + let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} /> + this._pageInfo.area.push(sticky); + } + this._toolOn = false; + } + this._interactive = true; + } + + /** + * starts drawing the line when user presses down. + */ + onDraw = () => { + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + + if (this._drawTool.current) { + this._currTool = this._drawTool.current; + if (this._drawToolOn) { + this._drawToolOn = false; + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.removeEventListener("pointerup", this.drawUp); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._drawTool.current.style.backgroundColor = "grey"; + } else { + this._drawToolOn = true; + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + this._drawTool.current.style.backgroundColor = "cyan"; + } + } + } + + + /** + * for changing color (for ink/pen) + */ + onColorChange = (e: React.PointerEvent) => { + if (e.currentTarget.innerHTML == "Red") { + this._currColor = "red"; + } else if (e.currentTarget.innerHTML == "Blue") { + this._currColor = "blue"; + } else if (e.currentTarget.innerHTML == "Green") { + this._currColor = "green"; + } else if (e.currentTarget.innerHTML == "Black") { + this._currColor = "black"; + } + + } + + + /** + * For highlighting (text drag highlighting) + */ + onHighlight = () => { + this._drawToolOn = false; + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + if (this._highlightTool.current) { + this._currTool = this._drawTool.current; + if (this._highlightToolOn) { + this._highlightToolOn = false; + this._highlightTool.current.style.backgroundColor = "grey"; + } else { + this._highlightToolOn = true; + this._highlightTool.current.style.backgroundColor = "orange"; + } + } + } + + + @action + saveThumbnail = () => { + setTimeout(() => { + var me = this; + htmlToImage.toPng(this._mainDiv.current!, + { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 }) + .then(function (dataUrl: string) { + me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + }, 1000); + } + + @action + onLoaded = (page: any) => { + if (this._mainDiv.current) { + this._mainDiv.current.childNodes.forEach((element) => { + if (element.nodeName == "DIV") { + element.childNodes[0].childNodes.forEach((e) => { + + if (e instanceof HTMLCanvasElement) { + this._pdfCanvas = e; + this._pdfContext = e.getContext("2d") + + } + + }) + } + }) + } + + // bcz: the number of pages should really be set when the document is imported. + this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages); + if (this._perPageInfo.length == 0) { //Makes sure it only runs once + this._perPageInfo = [...Array(page._transport.numPages)] + } + this._loaded = true; + } + + @action + setScaling = (r: any) => { + // bcz: the nativeHeight should really be set when the document is imported. + // also, the native dimensions could be different for different pages of the PDF + // so this design is flawed. + var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0); + if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) { + this.props.doc.SetNumber(KeyStore.NativeHeight, nativeWidth * r.entry.height / r.entry.width); + } + if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) { + this.saveThumbnail(); + } + } + + @computed + get pdfContent() { + let page = this.curPage; + if (page == 0) + page = 1; + const renderHeight = 2400; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; + return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> + <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}> + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> + </div> + } + </Measure> + </Document> + </div >; + } + + @computed + get pdfRenderer() { + let proxy = this._loaded ? (null) : this.imageProxyRenderer; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) { + return proxy; + } + return [ + this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), + this._currAnno.map((element: any) => element), + <div key="pdfBox-contentShell"> + {this.pdfContent} + {proxy} + </div> + ]; + } + + @computed + get imageProxyRenderer() { + let field = this.props.doc.Get(KeyStore.Thumbnail); + if (field) { + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; + return <img src={path} width="100%" />; + } + return (null); + } + + render() { + return ( + <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + {this.pdfRenderer} + </div > + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx new file mode 100644 index 000000000..d57dd5c0b --- /dev/null +++ b/src/client/views/nodes/Sticky.tsx @@ -0,0 +1,83 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import React = require("react") +import { observer } from "mobx-react" +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps { + Height: number; + Width: number; + X: number; + Y: number; +} + +/** + * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. + * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. + * + * Written By: Andrew Kim + */ +@observer +export class Sticky extends React.Component<IProps> { + + private initX: number = 0; + private initY: number = 0; + + private _ref = React.createRef<HTMLCanvasElement>(); + private ctx: any; //context that keeps track of sticky canvas + + /** + * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas + */ + drawDown = (e: React.PointerEvent) => { + if (this._ref.current) { + this.ctx = this._ref.current.getContext("2d"); + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + this.ctx.beginPath(); + this.ctx.lineTo(this.initX, this.initY); + this.ctx.strokeStyle = "black"; + document.addEventListener("pointermove", this.drawMove); + document.addEventListener("pointerup", this.drawUp); + } + } + + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this.ctx.lineTo(x, y); + this.ctx.stroke(); + + } + + /** + * when user lifts the mouse, the drawing ends + */ + drawUp = (e: PointerEvent) => { + this.ctx.closePath(); + console.log(this.ctx); + document.removeEventListener("pointermove", this.drawMove); + } + + render() { + return ( + <div onPointerDown={this.drawDown}> + <canvas ref={this._ref} height={this.props.Height} width={this.props.Width} + style={{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + background: "yellow", + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + opacity: 0.4 + }} + /> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss new file mode 100644 index 000000000..7306450d9 --- /dev/null +++ b/src/client/views/nodes/VideoBox.scss @@ -0,0 +1,4 @@ +.videobox-cont{ + width: 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx new file mode 100644 index 000000000..22ff5c5ad --- /dev/null +++ b/src/client/views/nodes/VideoBox.tsx @@ -0,0 +1,43 @@ +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { VideoField } from '../../../fields/VideoField'; +import "./VideoBox.scss" +import { ContextMenu } from "../../views/ContextMenu"; +import { observable, action } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class VideoBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(VideoBox) } + + constructor(props: FieldViewProps) { + super(props); + } + + + + componentDidMount() { + } + + componentWillUnmount() { + } + + + render() { + let field = this.props.doc.Get(this.props.fieldKey) + let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4": + field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4"; + + return ( + <div> + <video width = {200} height = {200} controls className = "videobox-cont"> + <source src = {path} type = "video/mp4"/> + Not supported. + </video> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/fields/AudioField.ts b/src/fields/AudioField.ts new file mode 100644 index 000000000..aefcc15c1 --- /dev/null +++ b/src/fields/AudioField.ts @@ -0,0 +1,31 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class AudioField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("http://techslides.com/demos/samples/sample.mp3") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + + ToScriptString(): string { + return `new AudioField("${this.Data}")`; + } + + Copy(): Field { + return new AudioField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Audio, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/fields/ImageField.ts b/src/fields/ImageField.ts index b2226d55a..be8d73e68 100644 --- a/src/fields/ImageField.ts +++ b/src/fields/ImageField.ts @@ -1,7 +1,6 @@ import { BasicField } from "./BasicField"; import { Field, FieldId } from "./Field"; import { Types } from "../server/Message"; -import { ObjectID } from "bson"; export class ImageField extends BasicField<URL> { constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts new file mode 100644 index 000000000..2a4ed18e7 --- /dev/null +++ b/src/fields/InkField.ts @@ -0,0 +1,53 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; +import { observable, ObservableMap } from "mobx"; + +export enum InkTool { + None, + Pen, + Highlighter, + Eraser +} +export interface StrokeData { + pathData: Array<{ x: number, y: number }>; + color: string; + width: string; + tool: InkTool; + page: number; +} +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, + } + } + + UpdateFromServer(data: any) { + this.data = new ObservableMap(data); + } + + static FromJson(id: string, data: any): InkField { + let map: StrokeMap = 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 27f0c28f5..f93a68c85 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -4,6 +4,7 @@ export namespace KeyStore { export const Prototype = new Key("Prototype"); export const X = new Key("X"); export const Y = new Key("Y"); + export const Page = new Key("Page"); export const Title = new Key("Title"); export const Author = new Key("Author"); export const PanX = new Key("PanX"); @@ -30,4 +31,8 @@ export namespace KeyStore { export const LinkedFromDocs = new Key("LinkedFromDocs"); export const LinkDescription = new Key("LinkDescription"); export const LinkTags = new Key("LinkTag"); + export const Thumbnail = new Key("Thumbnail"); + export const CurPage = new Key("CurPage"); + export const NumPages = new Key("NumPages"); + export const Ink = new Key("Ink"); } diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts index 700600804..ce32da0a6 100644 --- a/src/fields/ListField.ts +++ b/src/fields/ListField.ts @@ -40,6 +40,7 @@ export class ListField<T extends Field> extends BasicField<T[]> { this.observeDisposer() } this.data = observable(value); + this.updateProxies(); this.observeList(); } @@ -69,7 +70,25 @@ export class ListField<T extends Field> extends BasicField<T[]> { init(callback: (field: Field) => any) { Server.GetFields(this._proxies, action((fields: { [index: string]: Field }) => { if (!this.arraysEqual(this._proxies, this.Data.map(field => field.Id))) { - this.data = this._proxies.map(id => fields[id] as T) + var dataids = this.data.map(d => d.Id); + var added = this.data.length == this._proxies.length - 1; + var deleted = this.data.length > this._proxies.length; + for (let i = 0; i < dataids.length && added; i++) + added = this._proxies.indexOf(dataids[i]) != -1; + for (let i = 0; i < this._proxies.length && deleted; i++) + deleted = dataids.indexOf(this._proxies[i]) != -1; + if (added) { // if only 1 items was added + for (let i = 0; i < this._proxies.length; i++) + if (dataids.indexOf(this._proxies[i]) === -1) + this.Data.splice(i, 0, fields[this._proxies[i]] as T); + } else if (deleted) { // if only items were deleted + for (let i = this.data.length - 1; i >= 0; i--) { + if (this._proxies.indexOf(this.data[i].Id) === -1) { + this.Data.splice(i, 1); + } + } + } else // otherwise, just rebuild the whole list + this.data = this._proxies.map(id => fields[id] as T) observe(this.Data, () => { this.updateProxies() Server.UpdateField(this); diff --git a/src/fields/PDFField.ts b/src/fields/PDFField.ts new file mode 100644 index 000000000..f3a009001 --- /dev/null +++ b/src/fields/PDFField.ts @@ -0,0 +1,36 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { observable } from "mobx" +import { Types } from "../server/Message"; + + + +export class PDFField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("http://cs.brown.edu/~bcz/bob_fettucine.jpg") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + Copy(): Field { + return new PDFField(this.Data); + } + + ToScriptString(): string { + return `new PDFField("${this.Data}")`; + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.PDF, + data: this.Data, + _id: this.Id + } + } + + @observable + Page: Number = 1; + +}
\ No newline at end of file diff --git a/src/fields/VideoField.ts b/src/fields/VideoField.ts new file mode 100644 index 000000000..5f4ae19bf --- /dev/null +++ b/src/fields/VideoField.ts @@ -0,0 +1,30 @@ +import { BasicField } from "./BasicField"; +import { Field, FieldId } from "./Field"; +import { Types } from "../server/Message"; + +export class VideoField extends BasicField<URL> { + constructor(data: URL | undefined = undefined, id?: FieldId, save: boolean = true) { + super(data == undefined ? new URL("http://techslides.com/demos/sample-videos/small.mp4") : data, save, id); + } + + toString(): string { + return this.Data.href; + } + + ToScriptString(): string { + return `new VideoField("${this.Data}")`; + } + + Copy(): Field { + return new VideoField(this.Data); + } + + ToJson(): { type: Types, data: URL, _id: string } { + return { + type: Types.Video, + data: this.Data, + _id: this.Id + } + } + +}
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 148e6e723..8a00f6b59 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 + Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF } export class DocumentTransfer implements Transferable { diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index a53fb5d2b..5331c9e30 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -1,3 +1,4 @@ + import { Field } from './../fields/Field'; import { TextField } from './../fields/TextField'; import { NumberField } from './../fields/NumberField'; @@ -11,6 +12,13 @@ import { Types } from './Message'; import { Utils } from '../Utils'; import { HtmlField } from '../fields/HtmlField'; import { WebField } from '../fields/WebField'; +import { AudioField } from '../fields/AudioField'; +import { VideoField } from '../fields/VideoField'; +import {InkField} from '../fields/InkField'; +import {PDFField} from '../fields/PDFField'; + + + export class ServerUtils { public static FromJson(json: any): Field { @@ -39,8 +47,16 @@ export class ServerUtils { return new Key(data, id, false) case Types.Image: return new ImageField(new URL(data), id, false) + case Types.PDF: + return new PDFField(new URL(data), id, false) case Types.List: return ListField.FromJson(id, data) + case Types.Audio: + return new AudioField(new URL(data), id, false) + case Types.Video: + return new VideoField(new URL(data), id, false) + 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][] diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts new file mode 100644 index 000000000..e4a66f7f2 --- /dev/null +++ b/src/typings/index.d.ts @@ -0,0 +1,322 @@ +/// <reference types="node" /> + +declare module '@react-pdf/renderer' { + import * as React from 'react'; + + namespace ReactPDF { + interface Style { + [property: string]: any; + } + interface Styles { + [key: string]: Style; + } + type Orientation = 'portrait' | 'landscape'; + + interface DocumentProps { + title?: string; + author?: string; + subject?: string; + keywords?: string; + creator?: string; + producer?: string; + onRender?: () => any; + } + + /** + * This component represent the PDF document itself. It must be the root + * of your tree element structure, and under no circumstances should it be + * used as children of another react-pdf component. In addition, it should + * only have childs of type <Page />. + */ + class Document extends React.Component<DocumentProps> {} + + interface NodeProps { + style?: Style | Style[]; + /** + * Render component in all wrapped pages. + * @see https://react-pdf.org/advanced#fixed-components + */ + fixed?: boolean; + /** + * Force the wrapping algorithm to start a new page when rendering the + * element. + * @see https://react-pdf.org/advanced#page-breaks + */ + break?: boolean; + } + + interface PageProps extends NodeProps { + /** + * Enable page wrapping for this page. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + debug?: boolean; + size?: string | [number, number] | {width: number; height: number}; + orientation?: Orientation; + ruler?: boolean; + rulerSteps?: number; + verticalRuler?: boolean; + verticalRulerSteps?: number; + horizontalRuler?: boolean; + horizontalRulerSteps?: number; + ref?: Page; + } + + /** + * Represents single page inside the PDF document, or a subset of them if + * using the wrapping feature. A <Document /> can contain as many pages as + * you want, but ensure not rendering a page inside any component besides + * Document. + */ + class Page extends React.Component<PageProps> {} + + interface ViewProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + debug?: boolean; + render?: (props: {pageNumber: number}) => React.ReactNode; + children?: React.ReactNode; + } + + /** + * The most fundamental component for building a UI and is designed to be + * nested inside other views and can have 0 to many children. + */ + class View extends React.Component<ViewProps> {} + + interface ImageProps extends NodeProps { + debug?: boolean; + src: string | {data: Buffer; format: 'png' | 'jpg'}; + cache?: boolean; + } + + /** + * A React component for displaying network or local (Node only) JPG or + * PNG images, as well as base64 encoded image strings. + */ + class Image extends React.Component<ImageProps> {} + + interface TextProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + debug?: boolean; + render?: ( + props: {pageNumber: number; totalPages: number}, + ) => React.ReactNode; + children?: React.ReactNode; + /** + * How much hyphenated breaks should be avoided. + */ + hyphenationCallback?: number; + } + + /** + * A React component for displaying text. Text supports nesting of other + * Text or Link components to create inline styling. + */ + class Text extends React.Component<TextProps> {} + + interface LinkProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + debug?: boolean; + src: string; + children?: React.ReactNode; + } + + /** + * A React component for displaying an hyperlink. Link’s can be nested + * inside a Text component, or being inside any other valid primitive. + */ + class Link extends React.Component<LinkProps> {} + + interface NoteProps extends NodeProps { + children: string; + } + + class Note extends React.Component<NoteProps> {} + + interface BlobProviderParams { + blob: Blob | null; + url: string | null; + loading: boolean; + error: Error | null; + } + interface BlobProviderProps { + document: React.ReactElement<DocumentProps>; + children: (params: BlobProviderParams) => React.ReactNode; + } + + /** + * Easy and declarative way of getting document's blob data without + * showing it on screen. + * @see https://react-pdf.org/advanced#on-the-fly-rendering + * @platform web + */ + class BlobProvider extends React.Component<BlobProviderProps> {} + + interface PDFViewerProps { + width?: number; + height?: number; + style?: Style | Style[]; + className?: string; + children?: React.ReactElement<DocumentProps>; + } + + /** + * Iframe PDF viewer for client-side generated documents. + * @platform web + */ + class PDFViewer extends React.Component<PDFViewerProps> {} + + interface PDFDownloadLinkProps { + document: React.ReactElement<DocumentProps>; + fileName?: string; + style?: Style | Style[]; + className?: string; + children?: + | React.ReactNode + | ((params: BlobProviderParams) => React.ReactNode); + } + + /** + * Anchor tag to enable generate and download PDF documents on the fly. + * @see https://react-pdf.org/advanced#on-the-fly-rendering + * @platform web + */ + class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> {} + + interface EmojiSource { + url: string; + format: string; + } + interface RegisteredFont { + src: string; + loaded: boolean; + loading: boolean; + data: any; + [key: string]: any; + } + type HyphenationCallback = ( + words: string[], + glyphString: {[key: string]: any}, + ) => string[]; + + const Font: { + register: ( + src: string, + options: {family: string; [key: string]: any}, + ) => void; + getEmojiSource: () => EmojiSource; + getRegisteredFonts: () => string[]; + registerEmojiSource: (emojiSource: EmojiSource) => void; + registerHyphenationCallback: ( + hyphenationCallback: HyphenationCallback, + ) => void; + getHyphenationCallback: () => HyphenationCallback; + getFont: (fontFamily: string) => RegisteredFont | undefined; + load: ( + fontFamily: string, + document: React.ReactElement<DocumentProps>, + ) => Promise<void>; + clear: () => void; + reset: () => void; + }; + + const StyleSheet: { + hairlineWidth: number; + create: <TStyles>(styles: TStyles) => TStyles; + resolve: ( + style: Style, + container: { + width: number; + height: number; + orientation: Orientation; + }, + ) => Style; + flatten: (...styles: Style[]) => Style; + absoluteFillObject: { + position: 'absolute'; + left: 0; + right: 0; + top: 0; + bottom: 0; + }; + }; + + const version: any; + + const PDFRenderer: any; + + const createInstance: ( + element: { + type: string; + props: {[key: string]: any}; + }, + root?: any, + ) => any; + + const pdf: ( + document: React.ReactElement<DocumentProps>, + ) => { + isDirty: () => boolean; + updateContainer: (document: React.ReactElement<any>) => void; + toBuffer: () => NodeJS.ReadableStream; + toBlob: () => Blob; + toString: () => string; + }; + + const renderToStream: ( + document: React.ReactElement<DocumentProps>, + ) => NodeJS.ReadableStream; + + const renderToFile: ( + document: React.ReactElement<DocumentProps>, + filePath: string, + callback?: (output: NodeJS.ReadableStream, filePath: string) => any, + ) => Promise<NodeJS.ReadableStream>; + + const render: typeof renderToFile; + } + + const Document: typeof ReactPDF.Document; + const Page: typeof ReactPDF.Page; + const View: typeof ReactPDF.View; + const Image: typeof ReactPDF.Image; + const Text: typeof ReactPDF.Text; + const Link: typeof ReactPDF.Link; + const Note: typeof ReactPDF.Note; + const Font: typeof ReactPDF.Font; + const StyleSheet: typeof ReactPDF.StyleSheet; + const createInstance: typeof ReactPDF.createInstance; + const PDFRenderer: typeof ReactPDF.PDFRenderer; + const version: typeof ReactPDF.version; + const pdf: typeof ReactPDF.pdf; + + export default ReactPDF; + export { + Document, + Page, + View, + Image, + Text, + Link, + Note, + Font, + StyleSheet, + createInstance, + PDFRenderer, + version, + pdf, + }; + }
\ No newline at end of file |