diff options
73 files changed, 2379 insertions, 1515 deletions
diff --git a/package.json b/package.json index e658f8630..54355ecea 100644 --- a/package.json +++ b/package.json @@ -159,10 +159,10 @@ "jsx-to-string": "^1.4.0", "lodash": "^4.17.15", "mobile-detect": "^1.4.3", - "mobx": "^5.9.0", + "mobx": "^5.15.0", "mobx-react": "^5.3.5", "mobx-react-devtools": "^6.1.1", - "mobx-utils": "^5.4.0", + "mobx-utils": "^5.5.2", "mongodb": "^3.1.13", "mongoose": "^5.6.4", "node-sass": "^4.12.0", diff --git a/src/Utils.ts b/src/Utils.ts index 7bb025e49..37b509370 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -153,6 +153,63 @@ export namespace Utils { return Math.max(lower, Math.min(upper, n)); } + export function distanceBetweenHorizontalLines(xs: number, xe: number, y: number, xs2: number, xe2: number, y2: number): [number, number[]] { + if ((xs2 < xs && xe2 > xs) || (xs2 < xe && xe2 > xe) || (xs2 > xs && xe2 < xe)) return [Math.abs(y - y2), [Math.max(xs, xs2), y, Math.min(xe, xe2), y]]; + if (xe2 < xs) return [Math.sqrt((xe2 - xs) * (xe2 - xs) + (y2 - y) * (y2 - y)), [xs, y, xs, y]]; + //if (xs2 > xe) + return [Math.sqrt((xs2 - xe) * (xs2 - xe) + (y2 - y) * (y2 - y)), [xe, y, xe, y]]; + } + export function distanceBetweenVerticalLines(x: number, ys: number, ye: number, x2: number, ys2: number, ye2: number): [number, number[]] { + if ((ys2 < ys && ye2 > ys) || (ys2 < ye && ye2 > ye) || (ys2 > ys && ye2 < ye)) return [Math.abs(x - x2), [x, Math.max(ys, ys2), x, Math.min(ye, ye2)]]; + if (ye2 < ys) return [Math.sqrt((ye2 - ys) * (ye2 - ys) + (x2 - x) * (x2 - x)), [x, ys, x, ys]]; + //if (ys2 > ye) + return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]]; + } + + function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) { + + if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 }; + var atob = { x: bx - ax, y: by - ay }; + var atop = { x: px - ax, y: py - ay }; + var len = atob.x * atob.x + atob.y * atob.y; + var dot = atop.x * atob.x + atop.y * atob.y; + var t = Math.min(1, Math.max(0, dot / len)); + + dot = (bx - ax) * (py - ay) - (by - ay) * (px - ax); + + return { + point: { + x: ax + atob.x * t, + y: ay + atob.y * t + }, + left: dot < 1, + dot: dot, + t: t + }; + } + + export function closestPtBetweenRectangles(l: number, t: number, w: number, h: number, + l1: number, t1: number, w1: number, h1: number, + x: number, y: number) { + var r = l + w, + b = t + h; + var r1 = l1 + w1, + b1 = t1 + h1; + let hsegs = [[l, r, t, l1, r1, t1], [l, r, b, l1, r1, t1], [l, r, t, l1, r1, b1], [l, r, b, l1, r1, b1]]; + let vsegs = [[l, t, b, l1, t1, b1], [r, t, b, l1, t1, b1], [l, t, b, r1, t1, b1], [r, t, b, r1, t1, b1]]; + let res = hsegs.reduce((closest, seg) => { + let res = distanceBetweenHorizontalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]); + return (res[0] < closest[0]) ? res : closest; + }, [Number.MAX_VALUE, []] as [number, number[]]); + let fres = vsegs.reduce((closest, seg) => { + let res = distanceBetweenVerticalLines(seg[0], seg[1], seg[2], seg[3], seg[4], seg[5]); + return (res[0] < closest[0]) ? res : closest; + }, res); + + let near = project(x, y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]); + return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]); + } + export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) { var r = l + w, b = t + h; @@ -303,6 +360,8 @@ export function returnEmptyString() { return ""; } export function emptyFunction() { } +export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); } + export type Without<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type Predicate<K, V> = (entry: [K, V]) => boolean; diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 12501065a..f6dd0c346 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -24,5 +24,6 @@ export enum DocumentType { QUERY = "search", COLOR = "color", DOCULINK = "doculink", - PDFANNO = "pdfanno" + PDFANNO = "pdfanno", + INK = "ink" }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 92d6cd9cd..3981b6f78 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -48,6 +48,8 @@ import { PresElementBox } from "../views/presentationview/PresElementBox"; import { QueryBox } from "../views/nodes/QueryBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { DocuLinkBox } from "../views/nodes/DocuLinkBox"; +import { InkingStroke } from "../views/InkingStroke"; +import { InkField } from "../../new_fields/InkField"; var requestImageSize = require('../util/request-image-size'); var path = require('path'); @@ -96,7 +98,7 @@ export interface DocumentOptions { autoHeight?: boolean; removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document dbDoc?: Doc; - unchecked?: ScriptField; // returns whether a check box is unchecked + ischecked?: ScriptField; // returns whether a font icon box is checked activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts) onClick?: ScriptField; dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script @@ -108,6 +110,8 @@ export interface DocumentOptions { sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script dropConverter?: ScriptField; // script to run when documents are dropped on this Document. + strokeWidth?: number; + color?: string; // [key: string]: Opt<Field>; } @@ -210,6 +214,9 @@ export namespace Docs { [DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: data } }], + [DocumentType.INK, { + layout: { view: InkingStroke, dataField: data } + }] ]); // All document prototypes are initialized with at least these values @@ -415,6 +422,14 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options); } + export function InkDocument(color: string, tool: number, strokeWidth: number, points: { x: number, y: number }[], options: DocumentOptions = {}) { + let doc = InstanceFromProto(Prototypes.get(DocumentType.INK), new InkField(points), options); + doc.color = color; + doc.strokeWidth = strokeWidth; + doc.tool = tool; + return doc; + } + export function IconDocument(icon: string, options: DocumentOptions = {}) { return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options); } @@ -632,7 +647,7 @@ export namespace Docs { } if (type.indexOf("pdf") !== -1) { ctor = Docs.Create.PdfDocument; - options.nativeWidth = 1200; + options.nativeWidth = 927; options.nativeHeight = 1200; } if (type.indexOf("excel") !== -1) { diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.ts new file mode 100644 index 000000000..e58635a6f --- /dev/null +++ b/src/client/util/InteractionUtils.ts @@ -0,0 +1,111 @@ +export namespace InteractionUtils { + export const MOUSE = "mouse"; + export const TOUCH = "touch"; + + export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean { + return e.pointerType === type; + } + + export function TwoPointEuclidist(pt1: React.Touch, pt2: React.Touch): number { + return Math.sqrt(Math.pow(pt1.clientX - pt2.clientX, 2) + Math.pow(pt1.clientY - pt2.clientY, 2)); + } + + /** + * Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point) + * @param pts - n-arbitrary long list of points + */ + export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } { + let centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length; + let centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length; + return { X: centerX, Y: centerY }; + } + + /** + * Returns -1 if pinching out, 0 if not pinching, and 1 if pinching in + * @param pt1 - new point that corresponds to oldPoint1 + * @param pt2 - new point that corresponds to oldPoint2 + * @param oldPoint1 - previous point 1 + * @param oldPoint2 - previous point 2 + */ + export function Pinching(pt1: React.Touch, pt2: React.Touch, oldPoint1: React.Touch, oldPoint2: React.Touch): number { + let threshold = 4; + let oldDist = TwoPointEuclidist(oldPoint1, oldPoint2); + let newDist = TwoPointEuclidist(pt1, pt2); + + /** if they have the same sign, then we are either pinching in or out. + * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch) + * so that it can still pan without freaking out + */ + if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) { + return Math.sign(oldDist - newDist); + } + return 0; + } + + /** + * Returns the type of Touch Interaction from a list of points. + * Also returns any data that is associated with a Touch Interaction + * @param pts - List of points + */ + // export function InterpretPointers(pts: React.Touch[]): { type: Opt<TouchInteraction>, data?: any } { + // const leniency = 200; + // switch (pts.length) { + // case 1: + // return { type: OneFinger }; + // case 2: + // return { type: TwoSeperateFingers }; + // case 3: + // let pt1 = pts[0]; + // let pt2 = pts[1]; + // let pt3 = pts[2]; + // if (pt1 && pt2 && pt3) { + // let dist12 = TwoPointEuclidist(pt1, pt2); + // let dist23 = TwoPointEuclidist(pt2, pt3); + // let dist13 = TwoPointEuclidist(pt1, pt3); + // console.log(`distances: ${dist12}, ${dist23}, ${dist13}`); + // let dist12close = dist12 < leniency; + // let dist23close = dist23 < leniency; + // let dist13close = dist13 < leniency; + // let xor2313 = dist23close ? !dist13close : dist13close; + // let xor = dist12close ? !xor2313 : xor2313; + // // three input xor because javascript doesn't have logical xor's + // if (xor) { + // let points: number[] = []; + // let min = Math.min(dist12, dist23, dist13); + // switch (min) { + // case dist12: + // points = [0, 1, 2]; + // break; + // case dist23: + // points = [1, 2, 0]; + // break; + // case dist13: + // points = [0, 2, 1]; + // break; + // } + // return { type: TwoToOneFingers, data: points }; + // } + // else { + // return { type: ThreeSeperateFingers, data: null }; + // } + // } + // default: + // return { type: undefined }; + // } + // } + + export function IsDragging(oldTouches: Map<number, React.Touch>, newTouches: TouchList, leniency: number): boolean { + for (let i = 0; i < newTouches.length; i++) { + let touch = newTouches.item(i); + if (touch) { + let oldTouch = oldTouches.get(touch.identifier); + if (oldTouch) { + if (TwoPointEuclidist(touch, oldTouch) >= leniency) { + return true; + } + } + } + } + return false; + } +}
\ No newline at end of file diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 2d717ca57..e01216e0f 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,46 +1,44 @@ -import { observable, action, runInAction, IReactionDisposer, reaction, autorun } from "mobx"; -import { Doc, Opt } from "../../new_fields/Doc"; +import { observable, action, runInAction, ObservableMap } from "mobx"; +import { Doc } from "../../new_fields/Doc"; import { DocumentView } from "../views/nodes/DocumentView"; -import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { NumCast, StrCast } from "../../new_fields/Types"; -import { InkingControl } from "../views/InkingControl"; +import { computedFn } from "mobx-utils"; export namespace SelectionManager { class Manager { @observable IsDragging: boolean = false; - @observable SelectedDocuments: Array<DocumentView> = []; + SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap(); @action SelectDoc(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (manager.SelectedDocuments.indexOf(docView) === -1) { + if (!manager.SelectedDocuments.get(docView)) { if (!ctrlPressed) { this.DeselectAll(); } - manager.SelectedDocuments.push(docView); + manager.SelectedDocuments.set(docView, true); // console.log(manager.SelectedDocuments); docView.props.whenActiveChanged(true); - } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) { - manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false)); - manager.SelectedDocuments = [docView]; + } else if (!ctrlPressed && Array.from(manager.SelectedDocuments.entries()).length > 1) { + Array.from(manager.SelectedDocuments.keys()).map(dv => dv !== docView && dv.props.whenActiveChanged(false)); + manager.SelectedDocuments.clear(); + manager.SelectedDocuments.set(docView, true); } } @action DeselectDoc(docView: DocumentView): void { - let ind = manager.SelectedDocuments.indexOf(docView); - if (ind !== -1) { - manager.SelectedDocuments.splice(ind, 1); + if (manager.SelectedDocuments.get(docView)) { + manager.SelectedDocuments.delete(docView); docView.props.whenActiveChanged(false); } } @action DeselectAll(): void { - manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false)); - manager.SelectedDocuments = []; + Array.from(manager.SelectedDocuments.keys()).map(dv => dv.props.whenActiveChanged(false)); + manager.SelectedDocuments.clear(); } } @@ -53,14 +51,18 @@ export namespace SelectionManager { manager.SelectDoc(docView, ctrlPressed); } - export function IsSelected(doc: DocumentView): boolean { - return manager.SelectedDocuments.indexOf(doc) !== -1; + export function IsSelected(doc: DocumentView, outsideReaction?: boolean): boolean { + return outsideReaction ? + manager.SelectedDocuments.get(doc) ? true : false : + computedFn(function isSelected(doc: DocumentView) { + return manager.SelectedDocuments.get(doc) ? true : false; + })(doc); } export function DeselectAll(except?: Doc): void { let found: DocumentView | undefined = undefined; if (except) { - for (const view of manager.SelectedDocuments) { + for (const view of Array.from(manager.SelectedDocuments.keys())) { if (view.props.Document === except) found = view; } } @@ -73,6 +75,6 @@ export namespace SelectionManager { export function GetIsDragging() { return manager.IsDragging; } export function SelectedDocuments(): Array<DocumentView> { - return manager.SelectedDocuments.slice(); + return Array.from(manager.SelectedDocuments.keys()); } } diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss new file mode 100644 index 000000000..f3da5f284 --- /dev/null +++ b/src/client/views/AntimodeMenu.scss @@ -0,0 +1,29 @@ +.antimodeMenu-cont { + position: absolute; + z-index: 10000; + height: 35px; + background: #323232; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + border-radius: 0px 6px 6px 6px; + overflow: hidden; + display: flex; + + .antimodeMenu-button { + background-color: transparent; + width: 35px; + height: 35px; + } + + .antimodeMenu-button:hover { + background-color: #121212; + } + + .antimodeMenu-dragger { + height: 100%; + transition: width .2s; + background-image: url("https://logodix.com/logo/1020374.png"); + background-size: 90% 100%; + background-repeat: no-repeat; + background-position: left center; + } +}
\ No newline at end of file diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx new file mode 100644 index 000000000..408df8bc2 --- /dev/null +++ b/src/client/views/AntimodeMenu.tsx @@ -0,0 +1,128 @@ +import React = require("react"); +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import "./AntimodeMenu.scss"; + +/** + * This is an abstract class that serves as the base for a PDF-style or Marquee-style + * menu. To use this class, look at PDFMenu.tsx or MarqueeOptionsMenu.tsx for an example. + */ +export default abstract class AntimodeMenu extends React.Component { + protected _offsetY: number = 0; + protected _offsetX: number = 0; + protected _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + protected _dragging: boolean = false; + + @observable protected _top: number = -300; + @observable protected _left: number = -300; + @observable protected _opacity: number = 1; + @observable protected _transition: string = "opacity 0.5s"; + @observable protected _transitionDelay: string = ""; + + @observable public Pinned: boolean = false; + + @action + /** + * @param x + * @param y + * @param forceJump: If the menu is pinned down, do you want to force it to jump to the new location? + * Called when you want the menu to show up at a location + */ + public jumpTo = (x: number, y: number, forceJump: boolean = false) => { + if (!this.Pinned || forceJump) { + this._transition = this._transitionDelay = ""; + this._opacity = 1; + this._left = x; + this._top = y; + } + } + + @action + /** + * @param forceOut: Do you want the menu to disappear immediately or to slowly fadeout? + * Called when you want the menu to disappear + */ + public fadeOut = (forceOut: boolean) => { + if (!this.Pinned) { + if (this._opacity === 0.2) { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + + if (forceOut) { + this._transition = ""; + this._transitionDelay = ""; + this._opacity = 0; + this._left = this._top = -300; + } + } + } + + @action + protected pointerLeave = (e: React.PointerEvent) => { + if (!this.Pinned) { + this._transition = "opacity 0.5s"; + this._transitionDelay = "1s"; + this._opacity = 0.2; + setTimeout(() => this.fadeOut(false), 3000); + } + } + + @action + protected pointerEntered = (e: React.PointerEvent) => { + this._transition = "opacity 0.1s"; + this._transitionDelay = ""; + this._opacity = 1; + } + + @action + protected togglePin = (e: React.MouseEvent) => { + this.Pinned = !this.Pinned; + } + + protected dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.addEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointerup", this.dragEnd); + + this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; + this._offsetY = e.nativeEvent.offsetY; + + e.stopPropagation(); + e.preventDefault(); + } + + @action + protected dragging = (e: PointerEvent) => { + this._left = e.pageX - this._offsetX; + this._top = e.pageY - this._offsetY; + + e.stopPropagation(); + e.preventDefault(); + } + + protected dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + e.stopPropagation(); + e.preventDefault(); + } + + protected handleContextMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + } + + protected getElement(buttons: JSX.Element[]) { + return ( + <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} + style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> + {buttons} + <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss index 4423a7020..81210d7ae 100644 --- a/src/client/views/CollectionLinearView.scss +++ b/src/client/views/CollectionLinearView.scss @@ -17,6 +17,7 @@ height: 18px; margin-top:auto; margin-bottom:auto; + margin-right: 3px; cursor: pointer; transition: transform 0.2s; } @@ -51,8 +52,9 @@ .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { position:relative; - margin-top: auto; - margin-bottom: auto; + margin:auto; + margin-left: 3px; + transform-origin: center 80%; } .collectionLinearView-docBtn-scalable:hover { transform: scale(1.15); diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx index 7c6d33d36..f718735a8 100644 --- a/src/client/views/CollectionLinearView.tsx +++ b/src/client/views/CollectionLinearView.tsx @@ -22,18 +22,17 @@ const LinearDocument = makeInterface(documentSchema); export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef<HTMLInputElement>(); private _dropDisposer?: DragManager.DragDropDisposer; - private _heightDisposer?: IReactionDisposer; - private _spacing = 20; + private _widthDisposer?: IReactionDisposer; componentWillUnmount() { this._dropDisposer && this._dropDisposer(); - this._heightDisposer && this._heightDisposer(); + this._widthDisposer && this._widthDisposer(); } componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). - this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), - () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), + this._widthDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0), + () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), { fireImmediately: true } ); } @@ -52,6 +51,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); return new Transform(-translateX, -translateY, 1 / scale); } + render() { let guid = Utils.GenerateGuid(); return <div className="collectionLinearView-outer"> @@ -60,19 +60,16 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> - <div className="collectionLinearView-content"> + <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}> {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => { let nested = pair.layout.viewType === CollectionViewType.Linear; let dref = React.createRef<HTMLDivElement>(); let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension()); - let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth); - let scalingBox = nested ? 1 : this.dimension() / nativeWidth; - let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent; + let deltaSize = nativeWidth * .15 / 2; return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ - width: nested ? pair.layout[WidthSym]() : this.dimension(), - height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(), - transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)` + width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize, + height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, }} > <DocumentView Document={pair.layout} @@ -85,9 +82,9 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { ruleProvider={undefined} onClick={undefined} ScreenToLocalTransform={this.getTransform(dref)} - ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing - PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()} - PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()} + ContentScaling={returnOne} + PanelWidth={nested ? pair.layout[WidthSym] : () => this.dimension()}// ugh - need to get rid of this inline function to avoid recomputing + PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} backgroundColor={returnEmptyString} @@ -101,8 +98,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { </DocumentView> </div>; })} - {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */} - </div> </div> </div>; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 92c947fe6..1bd1006a8 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Doc } from '../../new_fields/Doc'; -import { computed, action } from 'mobx'; +import { Touchable } from './Touchable'; +import { computed, action, observable } from 'mobx'; import { Cast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { InkingControl } from './InkingControl'; @@ -13,7 +14,7 @@ interface DocComponentProps { Document: Doc; } export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) { - class Component extends React.Component<P> { + class Component extends Touchable<P> { //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document)); } @@ -26,7 +27,7 @@ interface DocExtendableProps { Document: Doc; DataDoc?: Doc; fieldKey: string; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; } export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) { @@ -36,7 +37,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt @computed get layoutDoc() { return Doc.Layout(this.props.Document); } @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; } @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } - active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools } return Component; } @@ -48,12 +49,12 @@ interface DocAnnotatableProps { DataDoc?: Doc; fieldKey: string; whenActiveChanged: (isActive: boolean) => void; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; renderDepth: number; } export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) { class Component extends React.Component<P> { - _isChildActive = false; + @observable _isChildActive = false; //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then @computed get Document(): T { return schemaCtor(this.props.Document); } @computed get layoutDoc() { return Doc.Layout(this.props.Document); } @@ -80,9 +81,11 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false; } - whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); - active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) && - (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false) + whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); + active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) && + (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) + annotationsActive = (outsideReaction?: boolean) => (InkingControl.Instance.selectedTool !== InkTool.None || + (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } return Component; }
\ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss index 8cd419bbe..db6bf2ba0 100644 --- a/src/client/views/DocumentButtonBar.scss +++ b/src/client/views/DocumentButtonBar.scss @@ -2,54 +2,23 @@ $linkGap : 3px; -.linkFlyout { +.documentButtonBar-linkFlyout { grid-column: 2/4; } -.linkButton-empty:hover { +.documentButtonBar-linkButton-empty:hover { background: $main-accent; transform: scale(1.05); cursor: pointer; } -.linkButton-nonempty:hover { +.documentButtonBar-linkButton-nonempty:hover { background: $main-accent; transform: scale(1.05); cursor: pointer; } - -.documentButtonBar { - margin-top: $linkGap; - grid-column: 1/4; - width: max-content; - height: auto; - display: flex; - flex-direction: row; -} - -.linkButtonWrapper { - pointer-events: auto; - padding-right: 5px; - width: 25px; -} - -.linkButton-linker { - height: 20px; - width: 20px; - text-align: center; - border-radius: 50%; - pointer-events: auto; - color: $dark-color; - border: $dark-color 1px solid; -} - -.linkButton-linker:hover { - cursor: pointer; - transform: scale(1.05); -} - -.linkButton-empty, -.linkButton-nonempty { +.documentButtonBar-linkButton-empty, +.documentButtonBar-linkButton-nonempty { height: 20px; width: 20px; border-radius: 50%; @@ -73,57 +42,38 @@ $linkGap : 3px; } } -.templating-menu { - position: absolute; - pointer-events: auto; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - transition: transform 0.2s; - text-align: center; +.documentButtonBar { + margin-top: $linkGap; + grid-column: 1/4; + width: max-content; + height: auto; display: flex; - justify-content: center; - align-items: center; + flex-direction: row; } -.templating-button, -.docDecs-tagButton { - width: 20px; +.documentButtonBar-button { + pointer-events: auto; + padding-right: 5px; + width: 25px; +} + +.documentButtonBar-linker { height: 20px; - border-radius: 50%; - opacity: 0.9; - font-size: 14; - background-color: $dark-color; - color: $light-color; + width: 20px; text-align: center; - cursor: pointer; - - &:hover { - background: $main-accent; - transform: scale(1.05); - } + border-radius: 50%; + pointer-events: auto; + color: $dark-color; + border: $dark-color 1px solid; + transition: 0.2s ease all; } -#template-list { - position: absolute; - top: 25px; - left: 0px; - width: max-content; - font-family: $sans-serif; - font-size: 12px; - background-color: $light-color-secondary; - padding: 2px 12px; - list-style: none; - - .templateToggle, .chromeToggle { - text-align: left; - } - - input { - margin-right: 10px; - } +.documentButtonBar-linker:hover { + cursor: pointer; + transform: scale(1.05); } + @-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } @-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } @keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 1412316f9..1fefc70f1 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,20 +1,19 @@ import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, runInAction } from "mobx"; +import { action, observable, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../new_fields/Doc"; import { RichTextField } from '../../new_fields/RichTextField'; import { NumCast, StrCast } from "../../new_fields/Types"; import { emptyFunction } from "../../Utils"; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; -import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; +import { DragManager } from "../util/DragManager"; import { LinkManager } from '../util/LinkManager'; import { UndoManager } from "../util/UndoManager"; import './DocumentButtonBar.scss'; import './collections/ParentDocumentSelector.scss'; import { LinkMenu } from "./linking/LinkMenu"; -import { MetadataEntryMenu } from './MetadataEntryMenu'; import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox"; import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; @@ -43,53 +42,53 @@ const fetch: IconProp = "sync-alt"; @observer export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> { private _linkButton = React.createRef<HTMLDivElement>(); - private _linkerButton = React.createRef<HTMLDivElement>(); - private _aliasButton = React.createRef<HTMLDivElement>(); - private _tooltipoff = React.createRef<HTMLDivElement>(); - private _textDoc?: Doc; + private _downX = 0; + private _downY = 0; + private _pullAnimating = false; + private _pushAnimating = false; + private _pullColorAnimating = false; + + @observable private pushIcon: IconProp = "arrow-alt-circle-up"; + @observable private pullIcon: IconProp = "arrow-alt-circle-down"; + @observable private pullColor: string = "white"; + @observable private isAnimatingFetch = false; + @observable private openHover = false; + public static Instance: DocumentButtonBar; + public static hasPushedHack = false; + public static hasPulledHack = false; constructor(props: { views: DocumentView[] }) { super(props); DocumentButtonBar.Instance = this; } - @observable public pushIcon: IconProp = "arrow-alt-circle-up"; - @observable public pullIcon: IconProp = "arrow-alt-circle-down"; - @observable public pullColor: string = "white"; - @observable public isAnimatingFetch = false; - @observable public openHover = false; - public pullColorAnimating = false; - - private pullAnimating = false; - private pushAnimating = false; - public startPullOutcome = action((success: boolean) => { - if (!this.pullAnimating) { - this.pullAnimating = true; + if (!this._pullAnimating) { + this._pullAnimating = true; this.pullIcon = success ? "check-circle" : "stop-circle"; setTimeout(() => runInAction(() => { this.pullIcon = "arrow-alt-circle-down"; - this.pullAnimating = false; + this._pullAnimating = false; }), 1000); } }); public startPushOutcome = action((success: boolean) => { - if (!this.pushAnimating) { - this.pushAnimating = true; + if (!this._pushAnimating) { + this._pushAnimating = true; this.pushIcon = success ? "check-circle" : "stop-circle"; setTimeout(() => runInAction(() => { this.pushIcon = "arrow-alt-circle-up"; - this.pushAnimating = false; + this._pushAnimating = false; }), 1000); } }); public setPullState = action((unchanged: boolean) => { this.isAnimatingFetch = false; - if (!this.pullColorAnimating) { - this.pullColorAnimating = true; + if (!this._pullColorAnimating) { + this._pullColorAnimating = true; this.pullColor = unchanged ? "lawngreen" : "red"; setTimeout(this.clearPullColor, 1000); } @@ -97,49 +96,20 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[], private clearPullColor = action(() => { this.pullColor = "white"; - this.pullColorAnimating = false; + this._pullColorAnimating = false; }); - onLinkerButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.addEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - document.addEventListener("pointerup", this.onLinkerButtonUp); - } - - onAliasButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.addEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - document.addEventListener("pointerup", this.onAliasButtonUp); - } - - onLinkerButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); - e.stopPropagation(); - } - - onAliasButtonUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - e.stopPropagation(); - } @action - onLinkerButtonMoved = (e: PointerEvent): void => { - if (this._linkerButton.current !== null) { - document.removeEventListener("pointermove", this.onLinkerButtonMoved); - document.removeEventListener("pointerup", this.onLinkerButtonUp); + onLinkButtonMoved = (e: PointerEvent): void => { + if (this._linkButton.current !== null && (Math.abs(e.clientX - this._downX) > 3 || Math.abs(e.clientY - this._downY) > 3)) { + document.removeEventListener("pointermove", this.onLinkButtonMoved); + document.removeEventListener("pointerup", this.onLinkButtonUp); let docView = this.props.views[0]; - let container = docView.props.ContainingCollectionDoc ? docView.props.ContainingCollectionDoc.proto : undefined; + let container = docView.props.ContainingCollectionDoc?.proto; let dragData = new DragManager.LinkDragData(docView.props.Document, container ? [container] : []); let linkDrag = UndoManager.StartBatch("Drag Link"); - DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { + DragManager.StartLinkDrag(this._linkButton.current, dragData, e.pageX, e.pageY, { handlers: { dragComplete: () => { let tooltipmenu = FormattedTextBox.ToolTipTextMenu; @@ -163,37 +133,15 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[], e.stopPropagation(); } - @action - onAliasButtonMoved = (e: PointerEvent): void => { - if (this._aliasButton.current !== null) { - document.removeEventListener("pointermove", this.onAliasButtonMoved); - document.removeEventListener("pointerup", this.onAliasButtonUp); - - let dragDocView = this.props.views[0]; - let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); - const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); - dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top); - dragData.embedDoc = true; - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, e.x, e.y, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } - e.stopPropagation(); - } onLinkButtonDown = (e: React.PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); + this._downX = e.clientX; + this._downY = e.clientY; document.removeEventListener("pointermove", this.onLinkButtonMoved); document.addEventListener("pointermove", this.onLinkButtonMoved); document.removeEventListener("pointerup", this.onLinkButtonUp); document.addEventListener("pointerup", this.onLinkButtonUp); + e.stopPropagation(); } onLinkButtonUp = (e: PointerEvent): void => { @@ -202,173 +150,91 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[], e.stopPropagation(); } - onLinkButtonMoved = async (e: PointerEvent) => { - if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) { - document.removeEventListener("pointermove", this.onLinkButtonMoved); - document.removeEventListener("pointerup", this.onLinkButtonUp); - DragLinksAsDocuments(this._linkButton.current, e.x, e.y, this.props.views[0].props.Document); - } - e.stopPropagation(); + @computed + get considerGoogleDocsPush() { + let targetDoc = this.props.views[0].props.Document; + let published = Doc.GetProto(targetDoc)[GoogleRef] !== undefined; + return <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="documentButtonBar-linker" onClick={() => { + DocumentButtonBar.hasPushedHack = false; + targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; + }}> + <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "sm" : "xs"} /> + </div>; } - aliasDragger = () => { - return (<div className="linkButtonWrapper"> - <div title="Drag Alias" className="linkButton-linker" ref={this._aliasButton} onPointerDown={this.onAliasButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" /> - </div> - </div>); - } - - private get targetDoc() { - return this.props.views[0].props.Document; - } - - considerGoogleDocsPush = () => { - let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - if (!canPush) return (null); - let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined; - let icon: IconProp = published ? (this.pushIcon as any) : cloud; - return ( - <div className={"linkButtonWrapper"}> - <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { - DocumentButtonBar.hasPushedHack = false; - this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> - </div> - </div> - ); - } - - considerGoogleDocsPull = () => { - let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField; - let dataDoc = Doc.GetProto(this.targetDoc); - if (!canPull || !dataDoc[GoogleRef]) return (null); - let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch; - icon = this.openHover ? "share" : icon; + @computed + get considerGoogleDocsPull() { + let targetDoc = this.props.views[0].props.Document; + let dataDoc = Doc.GetProto(targetDoc); let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; - let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`; - return ( - <div className={"linkButtonWrapper"}> - <div - title={title} - className="linkButton-linker" - style={{ - backgroundColor: this.pullColor, - transition: "0.2s ease all" - }} - onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} - onPointerLeave={() => runInAction(() => this.openHover = false)} - onClick={e => { - if (e.altKey) { - e.preventDefault(); - window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); - } else { - this.clearPullColor(); - DocumentButtonBar.hasPulledHack = false; - this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1; - dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon - style={{ - WebkitAnimation: animation, - MozAnimation: animation - }} - className="documentdecorations-icon" - icon={icon} - size="sm" - /> - </div> - </div> - ); - } - - public static hasPushedHack = false; - public static hasPulledHack = false; - - considerTooltip = () => { - let thisDoc = this.props.views[0].props.Document; - let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; - if (!isTextDoc) return null; - this._textDoc = thisDoc; - return ( - <div className="tooltipwrapper"> - <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}> - {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */} - </div> - </div> - - ); - } - - onTooltipOff = (e: React.PointerEvent): void => { - e.stopPropagation(); - if (this._textDoc) { - if (this._tooltipoff.current) { - if (this._tooltipoff.current.title === "Hide Tooltip") { - this._tooltipoff.current.title = "Show Tooltip"; - this._textDoc.tooltip = "hi"; - } - else { - this._tooltipoff.current.title = "Hide Tooltip"; + return !dataDoc[GoogleRef] ? (null) : <div className="documentButtonBar-linker" + title={`${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`} + style={{ backgroundColor: this.pullColor }} + onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)} + onPointerLeave={action(() => this.openHover = false)} + onClick={e => { + if (e.altKey) { + e.preventDefault(); + window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`); + } else { + this.clearPullColor(); + DocumentButtonBar.hasPulledHack = false; + targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; + dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); } - } - } + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" + style={{ WebkitAnimation: animation, MozAnimation: animation }} + icon={this.openHover ? "share" : dataDoc.unchanged === false ? (this.pullIcon as any) : fetch} + /> + </div>; + } + + @computed + get linkButton() { + let linkCount = LinkManager.Instance.getAllRelatedLinks(this.props.views[0].props.Document).length; + return <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}> + <Flyout anchorPoint={anchorPoints.RIGHT_TOP} + content={<LinkMenu docView={this.props.views[0]} addDocTab={this.props.views[0].props.addDocTab} changeFlyout={emptyFunction} />}> + <div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} > + {linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />} + </div> + </Flyout> + </div>; } - get metadataMenu() { - return ( - <div className="linkButtonWrapper"> - <Flyout anchorPoint={anchorPoints.TOP_LEFT} - content={<MetadataEntryMenu docs={() => this.props.views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} - <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> - </Flyout> - </div> - ); + @computed + get contextButton() { + return <ParentDocSelector Views={this.props.views} Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => { + where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : + this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : + this.props.views[0].props.addDocTab(doc, data, "onRight"); + return true; + }} />; } render() { - let linkButton = null; - if (this.props.views.length > 0) { - let selFirst = this.props.views[0]; - - let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length; - linkButton = (<Flyout - anchorPoint={anchorPoints.RIGHT_TOP} - content={<LinkMenu docView={selFirst} - addDocTab={selFirst.props.addDocTab} - changeFlyout={emptyFunction} />}> - <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> - </Flyout >); - } - let templates: Map<Template, boolean> = new Map(); Array.from(Object.values(Templates.TemplateList)).map(template => templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean))); - return (<div className="documentButtonBar"> - <div className="linkButtonWrapper"> - <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> + let isText = this.props.views[0].props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field. need to add fieldKey to DocumentView + let considerPull = isText && this.considerGoogleDocsPull; + let considerPush = isText && this.considerGoogleDocsPush; + return <div className="documentButtonBar"> + <div className="documentButtonBar-button"> + {this.linkButton} </div> - <div className="linkButtonWrapper"> - <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}> - <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> - </div> - </div> - <div className="linkButtonWrapper"> + <div className="documentButtonBar-button"> <TemplateMenu docs={this.props.views} templates={templates} /> </div> - {this.metadataMenu} - {this.aliasDragger()} - {this.considerGoogleDocsPush()} - {this.considerGoogleDocsPull()} - <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight"); - return true; - }} /> - {/* {this.considerTooltip()} */} - </div> - ); + <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {this.considerGoogleDocsPush} + </div> + <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> + {this.considerGoogleDocsPull} + </div> + {this.contextButton} + </div>; } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 890c32bcb..66f47147f 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -24,6 +24,7 @@ import { DocumentView } from "./nodes/DocumentView"; import { FieldView } from "./nodes/FieldView"; import { IconBox } from "./nodes/IconBox"; import React = require("react"); +import { PointData } from '../../new_fields/InkField'; import { DocumentType } from '../documents/DocumentTypes'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; @@ -203,7 +204,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.removeEventListener("pointerup", this.onBackgroundUp); document.removeEventListener("pointermove", this.onTitleMove); document.removeEventListener("pointerup", this.onTitleUp); - DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, { + DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(documentView => documentView.ContentDiv!), dragData, e.x, e.y, { handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) }, hideSource: true }); @@ -462,7 +463,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> break; } - SelectionManager.SelectedDocuments().forEach(element => { + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); let layoutDoc = PositionDocument(Doc.Layout(element.props.Document)); @@ -508,7 +509,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false); } } - }); + })); } @action @@ -528,7 +529,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @computed get selectionTitle(): string { if (SelectionManager.SelectedDocuments().length === 1) { - let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey]; + let selected = SelectionManager.SelectedDocuments()[0]; + let field = selected.props.Document[this._fieldKey]; if (typeof field === "string") { return field; } @@ -560,6 +562,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } let minimizeIcon = ( <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}> + {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."} </div>); diff --git a/src/client/views/InkSelectDecorations.scss b/src/client/views/InkSelectDecorations.scss new file mode 100644 index 000000000..daff58fd6 --- /dev/null +++ b/src/client/views/InkSelectDecorations.scss @@ -0,0 +1,5 @@ +.inkSelectDecorations { + position: absolute; + border: black 1px solid; + z-index: 9001; +}
\ No newline at end of file diff --git a/src/client/views/InkSelectDecorations.tsx b/src/client/views/InkSelectDecorations.tsx new file mode 100644 index 000000000..d40df9b75 --- /dev/null +++ b/src/client/views/InkSelectDecorations.tsx @@ -0,0 +1,55 @@ +import React = require("react"); +import { Touchable } from "./Touchable"; +import { PointData } from "../../new_fields/InkField"; +import { observer } from "mobx-react"; +import { computed, observable, action, runInAction } from "mobx"; +import "./InkSelectDecorations.scss"; + +@observer +export default class InkSelectDecorations extends Touchable { + static Instance: InkSelectDecorations; + + @observable private _selectedInkNodes: Map<any, any> = new Map(); + + constructor(props: Readonly<{}>) { + super(props); + + InkSelectDecorations.Instance = this; + } + + @action + public SetSelected = (inkNodes: Map<any, any>, keepOld: boolean = false) => { + if (!keepOld) { + this._selectedInkNodes = new Map(); + } + inkNodes.forEach((value: any, key: any) => { + runInAction(() => this._selectedInkNodes.set(key, value)); + }); + } + + @computed + get Bounds(): { x: number, y: number, b: number, r: number } { + let left = Number.MAX_VALUE; + let top = Number.MAX_VALUE; + let right = -Number.MAX_VALUE; + let bottom = -Number.MAX_VALUE; + this._selectedInkNodes.forEach((value: PointData, key: string) => { + // value.pathData.map(val => { + // left = Math.min(val.x, left); + // top = Math.min(val.y, top); + // right = Math.max(val.x, right); + // bottom = Math.max(val.y, bottom); + // }); + }); + return { x: left, y: top, b: bottom, r: right }; + } + + render() { + let bounds = this.Bounds; + return <div style={{ + top: bounds.y, left: bounds.x, + height: bounds.b - bounds.y, + width: bounds.r - bounds.x + }} />; + } +}
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss deleted file mode 100644 index 8f32652ed..000000000 --- a/src/client/views/InkingCanvas.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import "globalCssVariables"; - -.inkingCanvas { - // opacity: 0.99; - touch-action: none; - - .jsx-parser { - position: absolute; - width: 100%; - height: 100%; - background: inherit; - //z-index: -1; // allows annotations to appear on videos when screen is full-size & ... - } -} - -.inkingCanvas-paths-ink, -.inkingCanvas-paths-markers, -.inkingCanvas-noSelect, -.inkingCanvas-canSelect { - position: absolute; - top: 0; - left: 0; - width: 8192px; - height: 8192px; - cursor: "crosshair"; - pointer-events: all; -} - -.inkingCanvas-canSelect, -.inkingCanvas-noSelect { - top: -50000px; - left: -50000px; - width: 100000px; - height: 100000px; -} - -.inkingCanvas-noSelect { - pointer-events: none; - cursor: "crosshair"; -} - -.inkingCanvas-paths-ink, -.inkingCanvas-paths-markers { - pointer-events: none; - z-index: 10000; // overlays ink on top of everything - cursor: "arrow"; -} - -.inkingCanvas-paths-markers { - mix-blend-mode: multiply; -}
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx deleted file mode 100644 index 0037b95d0..000000000 --- a/src/client/views/InkingCanvas.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import { action, computed, trace, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import { Utils } from "../../Utils"; -import { Transform } from "../util/Transform"; -import "./InkingCanvas.scss"; -import { InkingControl } from "./InkingControl"; -import { InkingStroke } from "./InkingStroke"; -import React = require("react"); -import { UndoManager } from "../util/UndoManager"; -import { StrokeData, InkField, InkTool } from "../../new_fields/InkField"; -import { Doc } from "../../new_fields/Doc"; -import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; - -interface InkCanvasProps { - getScreenTransform: () => Transform; - AnnotationDocument: Doc; - Document: Doc; - inkFieldKey: string; - children: () => JSX.Element[]; -} - -@observer -export class InkingCanvas extends React.Component<InkCanvasProps> { - maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome - @observable inkMidX: number = 0; - @observable inkMidY: number = 0; - private previousState?: Map<string, StrokeData>; - private _currentStrokeId: string = ""; - public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean { - return stroke.pathData.reduce((inside: boolean, val) => inside || - (selRect.left < val.x && selRect.left + selRect.width > val.x && - selRect.top < val.y && selRect.top + selRect.height > val.y) - , false); - } - public static StrokeRect(stroke: StrokeData): { left: number, top: number, right: number, bottom: number } { - return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) => - ({ - left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y), - right: Math.max(bounds.right, val.x), bottom: Math.max(bounds.bottom, val.y) - }) - , { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: -Number.MAX_VALUE, bottom: -Number.MAX_VALUE }); - } - - componentDidMount() { - PromiseValue(Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => { - if (ink) { - let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) => - strokeData.pathData.reduce(([mix, max, miy, may], p) => - [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)], - [mix, max, miy, may]), - [Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]); - this.inkMidX = (bounds[0] + bounds[1]) / 2; - this.inkMidY = (bounds[2] + bounds[3]) / 2; - } - })); - } - - @computed - get inkData(): Map<string, StrokeData> { - let map = Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField); - return !map ? new Map : new Map(map.inkData); - } - - set inkData(value: Map<string, StrokeData>) { - this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value); - } - - @action - onPointerDown = (e: React.PointerEvent): void => { - if (e.button !== 0 || e.altKey || e.ctrlKey || InkingControl.Instance.selectedTool === InkTool.None) { - return; - } - - document.addEventListener("pointermove", this.onPointerMove, true); - document.addEventListener("pointerup", this.onPointerUp, true); - e.stopPropagation(); - e.preventDefault(); - - this.previousState = new Map(this.inkData); - - if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - // start the new line, saves a uuid to represent the field of the stroke - this._currentStrokeId = Utils.GenerateGuid(); - const data = this.inkData; - data.set(this._currentStrokeId, { - pathData: [this.relativeCoordinatesForEvent(e.clientX, e.clientY)], - color: InkingControl.Instance.selectedColor, - width: InkingControl.Instance.selectedWidth, - tool: InkingControl.Instance.selectedTool, - displayTimecode: NumCast(this.props.Document.currentTimecode, -1), - creationTime: new Date().getTime() - }); - this.inkData = data; - } - } - - @action - onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove, true); - document.removeEventListener("pointerup", this.onPointerUp, true); - let coord = this.relativeCoordinatesForEvent(e.clientX, e.clientY); - if (Math.abs(coord.x - this.inkMidX) > 500 || Math.abs(coord.y - this.inkMidY) > 500) { - this.inkMidX = coord.x; - this.inkMidY = coord.y; - } - e.stopPropagation(); - e.preventDefault(); - - const batch = UndoManager.StartBatch("One ink stroke"); - const oldState = this.previousState || new Map; - this.previousState = undefined; - const newState = new Map(this.inkData); - UndoManager.AddEvent({ - undo: () => this.inkData = oldState, - redo: () => this.inkData = newState - }); - batch.end(); - } - - @action - onPointerMove = (e: PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - let data = this.inkData; // add points to new line as it is being drawn - let strokeData = data.get(this._currentStrokeId); - if (strokeData) { - strokeData.pathData.push(this.relativeCoordinatesForEvent(e.clientX, e.clientY)); - data.set(this._currentStrokeId, strokeData); - } - this.inkData = data; - } - } - - relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => { - let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey); - return { x, y }; - } - - @action - removeLine = (id: string): void => { - if (!this.previousState) { - this.previousState = new Map(this.inkData); - document.addEventListener("pointermove", this.onPointerMove, true); - document.addEventListener("pointerup", this.onPointerUp, true); - } - let data = this.inkData; - data.delete(id); - this.inkData = data; - } - - @computed - get drawnPaths() { - let curTimecode = NumCast(this.props.Document.currentTimecode, -1); - let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { - if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) { - paths.push(<InkingStroke key={id} id={id} - line={strokeData.pathData} - count={strokeData.pathData.length} - offsetX={this.maxCanvasDim - this.inkMidX} - offsetY={this.maxCanvasDim - this.inkMidY} - color={strokeData.color} - width={strokeData.width} - tool={strokeData.tool} - creationTime={strokeData.creationTime} - deleteCallback={this.removeLine} />); - } - return paths; - }, [] as JSX.Element[]); - let markerPaths = paths.filter(path => path.props.tool === InkTool.Highlighter); - let penPaths = paths.filter(path => path.props.tool !== InkTool.Highlighter); - return [!penPaths.length ? (null) : - <svg className={`inkingCanvas-paths-ink`} key="Pens" - style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} > - {penPaths} - </svg>, - !markerPaths.length ? (null) : - <svg className={`inkingCanvas-paths-markers`} key="Markers" - style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}> - {markerPaths} - </svg>]; - } - - render() { - let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect"; - let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser || - InkingControl.Instance.selectedTool === InkTool.Scrubber ? "pointer" : "default") : undefined; - return ( - <div className="inkingCanvas"> - <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} style={{ cursor: cursor }} /> - {this.props.children()} - {this.drawnPaths} - </div > - ); - } -}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 332c22512..723b8cac4 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,70 +1,58 @@ +import { computed } from "mobx"; import { observer } from "mobx-react"; -import { observable, trace, runInAction } from "mobx"; +import { documentSchema } from "../../new_fields/documentSchemas"; +import { InkData, InkField, InkTool } from "../../new_fields/InkField"; +import { makeInterface } from "../../new_fields/Schema"; +import { Cast } from "../../new_fields/Types"; +import { DocExtendableComponent } from "./DocComponent"; import { InkingControl } from "./InkingControl"; -import React = require("react"); -import { InkTool } from "../../new_fields/InkField"; import "./InkingStroke.scss"; -import { AudioBox } from "./nodes/AudioBox"; - +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import React = require("react"); -interface StrokeProps { - offsetX: number; - offsetY: number; - id: string; - count: number; - line: Array<{ x: number, y: number }>; - color: string; - width: string; - tool: InkTool; - creationTime: number; - deleteCallback: (index: string) => void; +type InkDocument = makeInterface<[typeof documentSchema]>; +const InkDocument = makeInterface(documentSchema); + +export function CreatePolyline(points: { x: number, y: number }[], left: number, top: number, color?: string, width?: number) { + let pts = points.reduce((acc: string, pt: { x: number, y: number }) => acc + `${pt.x - left},${pt.y - top} `, ""); + return ( + <polyline + points={pts} + style={{ + fill: "none", + stroke: color ?? InkingControl.Instance.selectedColor, + strokeWidth: width ?? InkingControl.Instance.selectedWidth + }} + /> + ); } @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; - - deleteStroke = (e: React.PointerEvent): void => { - if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) { - this.props.deleteCallback(this.props.id); - e.stopPropagation(); - e.preventDefault(); - } - if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) { - AudioBox.SetScrubTime(this.props.creationTime); - e.stopPropagation(); - e.preventDefault(); - } - } +export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocument>(InkDocument) { + public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } - parseData = (line: Array<{ x: number, y: number }>): string => { - return !line.length ? "" : "M " + line.map(p => (p.x + this.props.offsetX) + " " + (p.y + this.props.offsetY)).join(" L "); - } - - createStyle() { - switch (this._strokeTool) { - // add more tool styles here - default: - return { - fill: "none", - stroke: this._strokeColor, - strokeWidth: this._strokeWidth + "px", - }; - } - } + @computed get PanelWidth() { return this.props.PanelWidth(); } + @computed get PanelHeight() { return this.props.PanelHeight(); } render() { - let pathStyle = this.createStyle(); - let pathData = this.parseData(this.props.line); - let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes - let marker = this.props.tool === InkTool.Highlighter ? "-marker" : ""; - - let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser || - InkingControl.Instance.selectedTool === InkTool.Scrubber ? "all" : "none"; - return (<path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} - strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />); + let data: InkData = Cast(this.Document.data, InkField)?.inkData ?? []; + let xs = data.map(p => p.x); + let ys = data.map(p => p.y); + let left = Math.min(...xs); + let top = Math.min(...ys); + let right = Math.max(...xs); + let bottom = Math.max(...ys); + let points = CreatePolyline(data, 0, 0, this.Document.color, this.Document.strokeWidth); + let width = right - left; + let height = bottom - top; + let scaleX = this.PanelWidth / width; + let scaleY = this.PanelHeight / height; + return <svg width={width} height={height} style={{ + transformOrigin: "top left", + transform: `translate(${left}px, ${top}px) scale(${scaleX}, ${scaleY})`, + mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset" + }}> + {points} + </svg>; } }
\ No newline at end of file diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 134a4ac85..3b66160fb 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -5,7 +5,7 @@ html, body { width: 100%; height: 100%; - overflow: auto; + overflow: hidden; font-family: $sans-serif; margin: 0; position: absolute; @@ -27,7 +27,7 @@ div { pointer-events: none; border-radius: inherit; position: inherit; - background: inherit; + // background: inherit; } p { @@ -38,7 +38,7 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; height: 8px; - width: 8px; + width: 20px; } ::-webkit-scrollbar-thumb { @@ -65,10 +65,6 @@ button:hover { cursor: pointer; } -#root { - overflow: visible; -} - .svg-inline--fa { vertical-align: unset; }
\ No newline at end of file diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a858a73c7..0ee30f117 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -4,8 +4,9 @@ .mainView-tabButtons { position: relative; - width:100%; + width: 100%; } + // add nodes menu. Note that the + button is actually an input label, not an actual button. .mainView-docButtons { position: absolute; @@ -19,24 +20,27 @@ position: absolute; top: 0; left: 0; - overflow: auto; z-index: 1; } + .mainView-mainContent { - width:100%; - height:100%; - position:absolute; + width: 100%; + height: 100%; + position: absolute; } -.mainView-flyoutContainer{ - display:flex; + +.mainView-flyoutContainer { + display: flex; flex-direction: column; position: absolute; - width:100%; - height:100%; + width: 100%; + height: 100%; + .documentView-node-topmost { background: lightgrey; } } + .mainView-mainDiv { width: 100%; height: 100%; @@ -62,8 +66,8 @@ .mainView-expandFlyoutButton { position: absolute; - top: 5px; - right: 5px; + top: 100px; + right: 30px; cursor: pointer; } @@ -75,6 +79,7 @@ border-radius: 5px; position: absolute; z-index: 1; + touch-action: none; } .mainView-workspace { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 90b3d4c1b..268ccffbe 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, - faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone + faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; @@ -36,6 +36,8 @@ import { DocumentView } from './nodes/DocumentView'; import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; +import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; +import InkSelectDecorations from './InkSelectDecorations'; import { Scripting } from '../util/Scripting'; import { AudioBox } from './nodes/AudioBox'; import {Timeline} from './animationtimeline/Timeline'; @@ -117,6 +119,7 @@ export class MainView extends React.Component { library.add(faMusic); library.add(faTree); library.add(faPlay); + library.add(faCompressArrowsAlt); library.add(faPause); library.add(faClone); library.add(faCut); @@ -131,6 +134,7 @@ export class MainView extends React.Component { library.add(faBolt); library.add(faChevronRight); library.add(faEllipsisV); + library.add(faMusic); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -300,13 +304,15 @@ export class MainView extends React.Component { } onPointerDown = (e: React.PointerEvent) => { - this._flyoutSizeOnDown = e.clientX; - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - e.preventDefault(); + if (this._flyoutTranslate) { + this._flyoutSizeOnDown = e.clientX; + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + e.stopPropagation(); + e.preventDefault(); + } } @action @@ -425,10 +431,10 @@ export class MainView extends React.Component { style={{ cursor: "ew-resize", left: `${(this.flyoutWidth * (this._flyoutTranslate ? 1 : 0)) - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }} onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}> <span title="library View Dragger" style={{ - width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "5vw", - height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "30vh", - position: "absolute", - top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "-10vh" + width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", + height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", + position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", + top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" }} /> </div> <div className="mainView-libraryFlyout" style={{ @@ -507,14 +513,16 @@ export class MainView extends React.Component { <SharingManager /> <GoogleAuthenticationManager /> <DocumentDecorations /> + <InkSelectDecorations /> {this.mainContent} <PreviewCursor /> <ContextMenu /> {this.docButtons} <PDFMenu /> + <MarqueeOptionsMenu /> <OverlayView /> <TimelineMenu/> </div >); } } -Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); });
\ No newline at end of file +Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss index 20f9b9a49..d384fd284 100644 --- a/src/client/views/PreviewCursor.scss +++ b/src/client/views/PreviewCursor.scss @@ -6,4 +6,5 @@ top: 0; left:0; pointer-events: none; + opacity: 1; }
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index eed2cc5da..136a272ab 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -1,4 +1,4 @@ -import { action, observable, runInAction } from 'mobx'; +import { action, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; @@ -7,21 +7,16 @@ import { Docs } from '../documents/Documents'; // import { Transform } from 'prosemirror-transform'; import { Doc } from '../../new_fields/Doc'; import { Transform } from "../util/Transform"; +import { TraceMobx } from '../../new_fields/util'; @observer export class PreviewCursor extends React.Component<{}> { - private _prompt = React.createRef<HTMLDivElement>(); static _onKeyPress?: (e: KeyboardEvent) => void; static _getTransform: () => Transform; static _addLiveTextDoc: (doc: Doc) => void; static _addDocument: (doc: Doc) => boolean; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; - //when focus is lost, this will remove the preview cursor - @action onBlur = (): void => { - PreviewCursor.Visible = false; - } - constructor(props: any) { super(props); document.addEventListener("keydown", this.onKeyPress); @@ -108,6 +103,12 @@ export class PreviewCursor extends React.Component<{}> { } } } + + //when focus is lost, this will remove the preview cursor + @action onBlur = (): void => { + PreviewCursor.Visible = false; + } + @action public static Show(x: number, y: number, onKeyPress: (e: KeyboardEvent) => void, @@ -119,18 +120,13 @@ export class PreviewCursor extends React.Component<{}> { this._addLiveTextDoc = addLiveText; this._getTransform = getTransform; this._addDocument = addDocument; - setTimeout(action(() => this.Visible = true), (1)); + this.Visible = true; } render() { - if (!PreviewCursor._clickPoint) { - return (null); - } - if (PreviewCursor.Visible && this._prompt.current) { - this._prompt.current.focus(); - } - return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._prompt} - style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}> - I + return (!PreviewCursor._clickPoint || !PreviewCursor.Visible) ? (null) : + <div className="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={e => e && e.focus()} + style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}> + I </div >; } }
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss new file mode 100644 index 000000000..186d3ab0d --- /dev/null +++ b/src/client/views/TemplateMenu.scss @@ -0,0 +1,50 @@ +@import "globalCssVariables"; +.templating-menu { + position: absolute; + pointer-events: auto; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + transition: transform 0.2s; + text-align: center; + display: flex; + justify-content: center; + align-items: center; +} + +.templating-button { + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + font-size: 14; + background-color: $dark-color; + color: $light-color; + text-align: center; + cursor: pointer; + + &:hover { + background: $main-accent; + transform: scale(1.05); + } +} + +.template-list { + position: absolute; + top: 25px; + left: 0px; + width: max-content; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + .templateToggle, .chromeToggle { + text-align: left; + } + + input { + margin-right: 10px; + } +}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 96265385e..c65b338b4 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -4,12 +4,13 @@ import { DocumentManager } from "../util/DocumentManager"; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; -import './DocumentDecorations.scss'; +import './TemplateMenu.scss'; import { DocumentView } from "./nodes/DocumentView"; import { Template, Templates } from "./Templates"; import React = require("react"); import { Doc } from "../../new_fields/Doc"; import { StrCast } from "../../new_fields/Types"; +import { emptyFunction } from "../../Utils"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -46,10 +47,13 @@ export interface TemplateMenuProps { templates: Map<Template, boolean>; } + @observer export class TemplateMenu extends React.Component<TemplateMenuProps> { @observable private _hidden: boolean = true; - dragRef = React.createRef<HTMLUListElement>(); + private _downx = 0; + private _downy = 0; + private _dragRef = React.createRef<HTMLUListElement>(); toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => { this.props.docs.map(dv => dv.setCustomView(e.target.checked)); @@ -122,6 +126,43 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled"); }); } + onAliasButtonUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + e.stopPropagation(); + } + + onAliasButtonDown = (e: React.PointerEvent): void => { + this._downx = e.clientX; + this._downy = e.clientY; + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.addEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + document.addEventListener("pointerup", this.onAliasButtonUp); + } + onAliasButtonMoved = (e: PointerEvent): void => { + if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) { + document.removeEventListener("pointermove", this.onAliasButtonMoved); + document.removeEventListener("pointerup", this.onAliasButtonUp); + + let dragDocView = this.props.docs[0]; + let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); + const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + dragData.embedDoc = true; + dragData.dropAction = "alias"; + DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { + offsetX: dragData.offset[0], + offsetY: dragData.offset[1], + handlers: { + dragComplete: action(emptyFunction), + }, + hideSource: false + }); + } + e.stopPropagation(); + } render() { let layout = Doc.Layout(this.props.docs[0].Document); @@ -132,9 +173,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />); templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />); return ( - <div className="templating-menu" > - <div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> - <ul id="template-list" ref={this.dragRef} style={{ display: this._hidden ? "none" : "block" }}> + <div className="templating-menu" onPointerDown={this.onAliasButtonDown}> + <div title="Drag:(create alias). Tap:(modify layout)." className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> + <ul className="template-list" ref={this._dragRef} style={{ display: this._hidden ? "none" : "block" }}> {templateMenu} {<button onClick={this.clearTemplates}>Restore Defaults</button>} </ul> diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx new file mode 100644 index 000000000..0dd4f734c --- /dev/null +++ b/src/client/views/Touchable.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { action } from 'mobx'; +import { InteractionUtils } from '../util/InteractionUtils'; + +export abstract class Touchable<T = {}> extends React.Component<T> { + protected _touchDrag: boolean = false; + protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>(); + + public FirstX: number = 0; + public FirstY: number = 0; + public SecondX: number = 0; + public SecondY: number = 0; + + /** + * When a touch even starts, we keep track of each touch that is associated with that event + */ + @action + protected onTouchStart = (e: React.TouchEvent): void => { + for (let i = 0; i < e.targetTouches.length; i++) { + let pt = e.targetTouches.item(i); + this.prevPoints.set(pt.identifier, pt); + } + + switch (e.targetTouches.length) { + case 1: + this.handle1PointerDown(e); + break; + case 2: + this.handle2PointersDown(e); + } + + document.removeEventListener("touchmove", this.onTouch); + document.addEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + document.addEventListener("touchend", this.onTouchEnd); + } + + /** + * Handle touch move event + */ + @action + protected onTouch = (e: TouchEvent): void => { + // if we're not actually moving a lot, don't consider it as dragging yet + if (!InteractionUtils.IsDragging(this.prevPoints, e.targetTouches, 5) && !this._touchDrag) return; + this._touchDrag = true; + switch (e.targetTouches.length) { + case 1: + this.handle1PointerMove(e); + break; + case 2: + this.handle2PointersMove(e); + break; + } + } + + @action + protected onTouchEnd = (e: TouchEvent): void => { + this._touchDrag = false; + e.stopPropagation(); + + // remove all the touches associated with the event + for (let i = 0; i < e.targetTouches.length; i++) { + let pt = e.targetTouches.item(i); + if (pt) { + if (this.prevPoints.has(pt.identifier)) { + this.prevPoints.delete(pt.identifier); + } + } + } + + if (e.targetTouches.length === 0) { + this.prevPoints.clear(); + } + this.cleanUpInteractions(); + } + + cleanUpInteractions = (): void => { + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + handle1PointerMove = (e: TouchEvent): any => { + e.stopPropagation(); + e.preventDefault(); + } + + handle2PointersMove = (e: TouchEvent): any => { + e.stopPropagation(); + e.preventDefault(); + } + + handle1PointerDown = (e: React.TouchEvent): any => { }; + handle2PointersDown = (e: React.TouchEvent): any => { }; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 12f54d69d..bcdc9c97e 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,12 +1,13 @@ @import "../../views/globalCssVariables.scss"; -.lm_active .messageCounter{ - color:white; +.lm_active .messageCounter { + color: white; background: #999999; } + .messageCounter { - width:18px; - height:20px; + width: 18px; + height: 20px; text-align: center; border-radius: 20px; margin-left: 5px; @@ -18,25 +19,33 @@ .collectiondockingview-container { width: 100%; - height:100%; + height: 100%; border-style: solid; border-width: $COLLECTION_BORDER_WIDTH; position: absolute; top: 0; left: 0; overflow: hidden; + + .collectionDockingView-dragAsDocument { + touch-action: none; + } + .lm_content { background: white; } + .lm_controls>li { opacity: 0.6; transform: scale(1.2); } + .lm_maximised .lm_controls .lm_maximise { opacity: 1; transform: scale(0.8); background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAKElEQVR4nGP8////fwYCgImQAgYGBgYWKM2IR81/okwajIpgvsMbVgAwgQYRVakEKQAAAABJRU5ErkJggg==) !important; } + .flexlayout__layout { left: 0; top: 0; @@ -45,17 +54,21 @@ position: absolute; overflow: hidden; } + .flexlayout__splitter { background-color: black; } + .flexlayout__splitter:hover { background-color: #333; } + .flexlayout__splitter_drag { border-radius: 5px; background-color: #444; z-index: 1000; } + .flexlayout__outline_rect { position: absolute; cursor: move; @@ -65,6 +78,7 @@ z-index: 1000; box-sizing: border-box; } + .flexlayout__outline_rect_edge { cursor: move; border: 2px solid #b7d1b5; @@ -73,12 +87,14 @@ z-index: 1000; box-sizing: border-box; } + .flexlayout__edge_rect { position: absolute; z-index: 1000; box-shadow: inset 0 0 5px rgba(0, 0, 0, .2); background-color: lightgray; } + .flexlayout__drag_rect { position: absolute; cursor: move; @@ -97,11 +113,13 @@ padding: 10px; word-wrap: break-word; } + .flexlayout__tabset { overflow: hidden; background-color: #222; box-sizing: border-box; } + .flexlayout__tab { overflow: auto; position: absolute; @@ -109,6 +127,7 @@ background-color: #222; color: black; } + .flexlayout__tab_button { cursor: pointer; padding: 2px 8px 3px 8px; @@ -120,28 +139,35 @@ vertical-align: top; box-sizing: border-box; } + .flexlayout__tab_button--selected { color: #ddd; background-color: #222; } + .flexlayout__tab_button--unselected { color: gray; } + .flexlayout__tab_button_leading { display: inline-block; } + .flexlayout__tab_button_content { display: inline-block; } + .flexlayout__tab_button_textbox { float: left; border: none; color: lightgreen; background-color: #222; } + .flexlayout__tab_button_textbox:focus { outline: none; } + .flexlayout__tab_button_trailing { display: inline-block; margin-left: 5px; @@ -149,10 +175,12 @@ width: 8px; height: 8px; } + .flexlayout__tab_button:hover .flexlayout__tab_button_trailing, .flexlayout__tab_button--selected .flexlayout__tab_button_trailing { background: transparent url("../../../../node_modules/flexlayout-react/images/close_white.png") no-repeat center; } + .flexlayout__tab_button_overflow { float: left; width: 20px; @@ -165,6 +193,7 @@ font-family: Arial, sans-serif; background: transparent url("../../../../node_modules/flexlayout-react/images/more.png") no-repeat left; } + .flexlayout__tabset_header { position: absolute; left: 0; @@ -175,6 +204,7 @@ /*box-shadow: inset 0px 0px 3px 0px rgba(136, 136, 136, 0.54);*/ box-sizing: border-box; } + .flexlayout__tab_header_inner { position: absolute; left: 0; @@ -182,6 +212,7 @@ bottom: 0; width: 10000px; } + .flexlayout__tab_header_outer { background-color: black; position: absolute; @@ -191,12 +222,15 @@ /*height: 100px;*/ overflow: hidden; } + .flexlayout__tabset-selected { background-image: linear-gradient(#111, #444); } + .flexlayout__tabset-maximized { background-image: linear-gradient(#666, #333); } + .flexlayout__tab_toolbar { position: absolute; display: flex; @@ -206,6 +240,7 @@ bottom: 0; right: 0; } + .flexlayout__tab_toolbar_button-min { width: 20px; height: 20px; @@ -213,6 +248,7 @@ outline-width: 0; background: transparent url("../../../../node_modules/flexlayout-react/images/maximize.png") no-repeat center; } + .flexlayout__tab_toolbar_button-max { width: 20px; height: 20px; @@ -220,14 +256,18 @@ outline-width: 0; background: transparent url("../../../../node_modules/flexlayout-react/images/restore.png") no-repeat center; } + .flexlayout__popup_menu {} + .flexlayout__popup_menu_item { padding: 2px 10px 2px 10px; color: #ddd; } + .flexlayout__popup_menu_item:hover { background-color: #444444; } + .flexlayout__popup_menu_container { box-shadow: inset 0 0 5px rgba(0, 0, 0, .15); border: 1px solid #555; @@ -236,33 +276,39 @@ position: absolute; z-index: 1000; } + .flexlayout__border_top { background-color: black; border-bottom: 1px solid #ddd; box-sizing: border-box; overflow: hidden; } + .flexlayout__border_bottom { background-color: black; border-top: 1px solid #333; box-sizing: border-box; overflow: hidden; } + .flexlayout__border_left { background-color: black; border-right: 1px solid #333; box-sizing: border-box; overflow: hidden; } + .flexlayout__border_right { background-color: black; border-left: 1px solid #333; box-sizing: border-box; overflow: hidden; } + .flexlayout__border_inner_bottom { display: flex; } + .flexlayout__border_inner_left { position: absolute; white-space: nowrap; @@ -270,6 +316,7 @@ transform-origin: top right; transform: rotate(-90deg); } + .flexlayout__border_inner_right { position: absolute; white-space: nowrap; @@ -277,6 +324,7 @@ transform-origin: top left; transform: rotate(90deg); } + .flexlayout__border_button { background-color: #222; color: white; @@ -288,29 +336,36 @@ vertical-align: top; box-sizing: border-box; } + .flexlayout__border_button--selected { color: #ddd; background-color: #222; } + .flexlayout__border_button--unselected { color: gray; } + .flexlayout__border_button_leading { float: left; display: inline; } + .flexlayout__border_button_content { display: inline-block; } + .flexlayout__border_button_textbox { float: left; border: none; color: green; background-color: #ddd; } + .flexlayout__border_button_textbox:focus { outline: none; } + .flexlayout__border_button_trailing { display: inline-block; margin-left: 5px; @@ -318,10 +373,12 @@ width: 8px; height: 8px; } + .flexlayout__border_button:hover .flexlayout__border_button_trailing, .flexlayout__border_button--selected .flexlayout__border_button_trailing { background: transparent url("../../../../node_modules/flexlayout-react/images/close_white.png") no-repeat center; } + .flexlayout__border_toolbar_left { position: absolute; display: flex; @@ -331,6 +388,7 @@ left: 0; right: 0; } + .flexlayout__border_toolbar_right { position: absolute; display: flex; @@ -340,6 +398,7 @@ left: 0; right: 0; } + .flexlayout__border_toolbar_top { position: absolute; display: flex; @@ -349,6 +408,7 @@ bottom: 0; right: 0; } + .flexlayout__border_toolbar_bottom { position: absolute; display: flex; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 42d372f4a..75d92105b 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -426,15 +426,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } tab.setActive(true); }; - ReactDOM.render(<span title="Drag as document" onPointerDown={ - e => { - e.preventDefault(); - e.stopPropagation(); - DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { - handlers: { dragComplete: emptyFunction }, - hideSource: false - }); - }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); + ReactDOM.render(<span title="Drag as document" + className="collectionDockingView-dragAsDocument" + onPointerDown={ + e => { + e.preventDefault(); + e.stopPropagation(); + DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, { + handlers: { dragComplete: emptyFunction }, + hideSource: false + }); + }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan); ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan); // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => { // where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc); @@ -446,7 +448,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.element.append(upDiv); tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => { tab.titleElement[0].textContent = doc.title, { fireImmediately: true }; - tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegree(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegree(doc)]} 1px`; + tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegreeUnmemoized(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegreeUnmemoized(doc)]} 1px`; }); //TODO why can't this just be doc instead of the id? tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; @@ -615,7 +617,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } - get layoutDoc() { return this._document && Doc.Layout(this._document);} + get layoutDoc() { return this._document && Doc.Layout(this._document); } panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth; panelHeight = () => this._panelHeight; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index ebd47fd19..65856cad3 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -112,7 +112,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { - if (this.props.isSelected()) e.stopPropagation(); + if (this.props.isSelected(true)) e.stopPropagation(); else { this.props.select(false); } @@ -201,7 +201,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { render() { return <div className="collectionSchemaView-container"> - <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}> + <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}> {this.schemaTable} </div> {this.dividerDragger} @@ -225,11 +225,11 @@ export interface SchemaTableProps { addDocument: (document: Doc) => boolean; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - active: () => boolean; + active: (outsideReaction: boolean) => boolean; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc) => boolean; setFocused: (document: Doc) => void; setPreviewDoc: (document: Doc) => void; @@ -442,14 +442,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onPointerDown = (e: React.PointerEvent): void => { this.props.setFocused(this.props.Document); - if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected()) { + if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { e.stopPropagation(); } } @action onKeyDown = (e: KeyboardEvent): void => { - if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) { + if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) { let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); @@ -778,7 +778,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } render() { - return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > + return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> </div>; diff --git a/src/client/views/collections/CollectionStaffView.scss b/src/client/views/collections/CollectionStaffView.scss new file mode 100644 index 000000000..493a5f670 --- /dev/null +++ b/src/client/views/collections/CollectionStaffView.scss @@ -0,0 +1,13 @@ +.collectionStaffView { + .collectionStaffView-staff { + width: 100%; + margin-top: 100px; + margin-bottom: 100px; + } + + .collectionStaffView-line { + margin: 10px; + height: 2px; + background: black; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx new file mode 100644 index 000000000..40e860b12 --- /dev/null +++ b/src/client/views/collections/CollectionStaffView.tsx @@ -0,0 +1,59 @@ +import { CollectionSubView } from "./CollectionSubView"; +import { Transform } from "../../util/Transform"; +import React = require("react"); +import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx"; +import { Doc, HeightSym } from "../../../new_fields/Doc"; +import { NumCast } from "../../../new_fields/Types"; +import "./CollectionStaffView.scss"; +import { observer } from "mobx-react"; + +@observer +export class CollectionStaffView extends CollectionSubView(doc => doc) { + private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(0, -this._mainCont.current!.scrollTop); + private _mainCont = React.createRef<HTMLDivElement>(); + private _reactionDisposer: IReactionDisposer | undefined; + @observable private _staves = NumCast(this.props.Document.staves); + + componentDidMount = () => { + this._reactionDisposer = reaction( + () => NumCast(this.props.Document.staves), + (staves) => runInAction(() => this._staves = staves) + ); + + this.props.Document.staves = 5; + } + + @computed get fieldExtensionDoc() { + return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey); + } + + @computed get addStaffButton() { + return <div onPointerDown={this.addStaff}>+</div>; + } + + @computed get staves() { + let staves = []; + for (let i = 0; i < this._staves; i++) { + let rows = []; + for (let j = 0; j < 5; j++) { + rows.push(<div key={`staff-${i}-${j}`} className="collectionStaffView-line"></div>); + } + staves.push(<div key={`staff-${i}`} className="collectionStaffView-staff"> + {rows} + </div>); + } + return staves; + } + + @action + addStaff = (e: React.PointerEvent) => { + this.props.Document.staves = this._staves + 1; + } + + render() { + return <div className="collectionStaffView" ref={this._mainCont}> + {this.staves} + {this.addStaffButton} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 0e3f0d1a9..8b993820b 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -49,7 +49,7 @@ export interface TreeViewProps { outerXf: () => { translateX: number, translateY: number }; treeViewId: string; parentKey: string; - active: () => boolean; + active: (outsideReaction?: boolean) => boolean; showHeaderFields: () => boolean; preventTreeViewOpen: boolean; renderedIds: string[]; @@ -130,7 +130,7 @@ class TreeView extends React.Component<TreeViewProps> { onPointerDown = (e: React.PointerEvent) => e.stopPropagation(); onPointerEnter = (e: React.PointerEvent): void => { - this.props.active() && Doc.BrushDoc(this.dataDoc); + this.props.active(true) && Doc.BrushDoc(this.dataDoc); if (e.buttons === 1 && SelectionManager.GetIsDragging()) { this._header!.current!.className = "treeViewItem-header"; document.addEventListener("pointermove", this.onDragMove, true); @@ -412,7 +412,7 @@ class TreeView extends React.Component<TreeViewProps> { pinToPres: (document: Doc) => void, screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, - active: () => boolean, + active: (outsideReaction?: boolean) => boolean, panelWidth: () => number, renderDepth: number, showHeaderFields: () => boolean, diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8d5694bf0..8387e95df 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,6 +17,7 @@ import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { CollectionLinearView } from '../CollectionLinearView'; +import { CollectionStaffView } from './CollectionStaffView'; import { DocumentType } from '../../documents/DocumentTypes'; import { ImageField } from '../../../new_fields/URLField'; import { DocListCast } from '../../../new_fields/Doc'; @@ -30,6 +31,7 @@ import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import './CollectionView.scss'; import { FieldViewProps, FieldView } from '../nodes/FieldView'; +import { Touchable } from '../Touchable'; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); export enum CollectionViewType { @@ -42,6 +44,7 @@ export enum CollectionViewType { Masonry, Pivot, Linear, + Staff } export namespace CollectionViewType { @@ -69,7 +72,7 @@ export interface CollectionRenderProps { } @observer -export class CollectionView extends React.Component<FieldViewProps> { +export class CollectionView extends Touchable<FieldViewProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } private _reactionDisposer: IReactionDisposer | undefined; @@ -110,7 +113,7 @@ export class CollectionView extends React.Component<FieldViewProps> { // bcz: Argh? What's the height of the collection chromes?? chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0); - active = () => this.props.isSelected() || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; + active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0; whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); }; @@ -165,6 +168,7 @@ export class CollectionView extends React.Component<FieldViewProps> { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); + case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } @@ -205,6 +209,7 @@ export class CollectionView extends React.Component<FieldViewProps> { this.props.Document.autoHeight = true; }, icon: "ellipsis-v" }); + subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" }); subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" }); subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" }); switch (this.props.Document.viewType) { @@ -220,7 +225,11 @@ export class CollectionView extends React.Component<FieldViewProps> { let layoutItems = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); - ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); + + let more = ContextMenu.Instance.findByDescription("More..."); + let moreItems = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); + !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index c186d15f8..aa25a900c 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -23,6 +23,12 @@ .parentDocumentSelector-button { pointer-events: all; } +.parentDocumentSelector-metadata { + pointer-events: auto; + padding-right: 5px; + width: 25px; + display: inline-block; +} .buttonSelector { position: absolute; display: inline-block; diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 8b6fa330c..4eb9e9d1e 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -13,10 +13,15 @@ import { DocumentManager } from "../../util/DocumentManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faEdit } from "@fortawesome/free-solid-svg-icons"; import { library } from "@fortawesome/fontawesome-svg-core"; +import { MetadataEntryMenu } from "../MetadataEntryMenu"; +import { DocumentView } from "../nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; library.add(faEdit); -type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; +type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void }; @observer export class SelectorContextMenu extends React.Component<SelectorProps> { @observable private _docs: { col: Doc, target: Doc }[] = []; @@ -53,16 +58,23 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc? }; } + get metadataMenu() { + return <div className="parentDocumentSelector-metadata"> + <Flyout anchorPoint={anchorPoints.TOP_LEFT} + content={<MetadataEntryMenu docs={() => this.props.Views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */} + <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div> + </Flyout> + </div>; + } render() { - return ( - <> - <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} - {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} - </> - ); + return <div > + <div key="metadata">Metadata: {this.metadataMenu}</div> + <p key="contexts">Contexts:</p> + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + {this._otherDocs.length ? <hr key="hr" /> : null} + {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)} + </div>; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 48d330674..e1d23ddcb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -6,6 +6,8 @@ import { ScriptField } from "../../../../new_fields/ScriptField"; import { OverlayView, OverlayElementOptions } from "../../OverlayView"; import { emptyFunction } from "../../../../Utils"; import React = require("react"); +import { ObservableMap, runInAction } from "mobx"; +import { Id } from "../../../../new_fields/FieldSymbols"; interface PivotData { type: string; @@ -31,8 +33,7 @@ export interface ViewDefResult { bounds?: ViewDefBounds; } -export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { - let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); +export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) { const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200); const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>(); @@ -49,6 +50,8 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: const docMap = new Map<Doc, ViewDefBounds>(); const groupNames: PivotData[] = []; + const expander = 1.05; + const gap = .15; let x = 0; pivotColumnGroups.forEach((val, key) => { let y = 0; @@ -58,25 +61,31 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: text: String(key), x, y: pivotAxisWidth + 50, - width: pivotAxisWidth * 1.25 * numCols, + width: pivotAxisWidth * expander * numCols, height: 100, fontSize: NumCast(pivotDoc.pivotFontSize, 10) }); for (const doc of val) { let layoutDoc = Doc.Layout(doc); + let wid = pivotAxisWidth; + let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth; + if (hgt > pivotAxisWidth) { + hgt = pivotAxisWidth; + wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth; + } docMap.set(doc, { - x: x + xCount * pivotAxisWidth * 1.25, + x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2, y: -y, - width: pivotAxisWidth, - height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth + width: wid, + height: hgt }); xCount++; if (xCount >= numCols) { - xCount = 0; - y += pivotAxisWidth * 1.25; + xCount = (pivotAxisWidth - wid) / 2; + y += pivotAxisWidth * expander; } } - x += pivotAxisWidth * 1.25 * (numCols + 1); + x += pivotAxisWidth * (numCols * expander + gap); }); childPairs.map(pair => { @@ -88,9 +97,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: height: NumCast(pair.layout.height) }; const pos = docMap.get(pair.layout) || defaultPosition; - layoutPoolData.set(pair, { transition: "transform 1s", ...pos }); + let data = poolData.get(pair.layout[Id]); + if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height) { + runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos })); + } }); - return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) }; + return { elements: viewDefsToJSX(groupNames) }; } export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 837413842..73b45edc6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -6,7 +6,8 @@ import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); import { DocumentType } from "../../../documents/DocumentTypes"; -import { observable, action } from "mobx"; +import { observable, action, reaction, IReactionDisposer } from "mobx"; +import { StrCast, Cast } from "../../../../new_fields/Types"; export interface CollectionFreeFormLinkViewProps { A: DocumentView; @@ -16,33 +17,57 @@ export interface CollectionFreeFormLinkViewProps { @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { - @observable _alive: number = 0; @observable _opacity: number = 1; + @observable _update: number = 0; + _anchorDisposer: IReactionDisposer | undefined; @action componentDidMount() { - this._alive = 1; - setTimeout(this.rerender, 50); - setTimeout(action(() => this._opacity = 0.05), 50); + setTimeout(action(() => this._opacity = 0.05), 750); + this._anchorDisposer = reaction(() => [this.props.A.props.ScreenToLocalTransform(), this.props.B.props.ScreenToLocalTransform()], + () => { + let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; + let adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); + let bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); + let a = adiv.getBoundingClientRect(); + let b = bdiv.getBoundingClientRect(); + let abounds = adiv.parentElement!.getBoundingClientRect(); + let bbounds = bdiv.parentElement!.getBoundingClientRect(); + let apt = Utils.closestPtBetweenRectangles(abounds.left, abounds.top, abounds.width, abounds.height, + bbounds.left, bbounds.top, bbounds.width, bbounds.height, + a.left + a.width / 2, a.top + a.height / 2); + let bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height, + abounds.left, abounds.top, abounds.width, abounds.height, + apt.point.x, apt.point.y); + let afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1"; + let bfield = afield === "anchor1" ? "anchor2" : "anchor1"; + this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; + this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; + this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; + this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; + this._update++; + } + , { fireImmediately: true }); } @action componentWillUnmount() { - this._alive = 0; + this._anchorDisposer?.(); } - rerender = action(() => { - if (this._alive) { - setTimeout(this.rerender, 50); - this._alive++; - } - }); render() { - let y = this._alive; + let y = this._update; let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : []; let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect(); let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect(); - let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2); - let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); + let apt = Utils.closestPtBetweenRectangles(a.left, a.top, a.width, a.height, + b.left, b.top, b.width, b.height, + a.left + a.width / 2, a.top + a.height / 2); + let bpt = Utils.closestPtBetweenRectangles(b.left, b.top, b.width, b.height, + a.left, a.top, a.width, a.height, + apt.point.x, apt.point.y); + let pt1 = [apt.point.x, apt.point.y]; + let pt2 = [bpt.point.x, bpt.point.y]; return (<line key="linkLine" className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity }} x1={`${pt1[0]}`} y1={`${pt1[1]}`} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index bb1a12f88..070d4aa65 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,5 +1,6 @@ @import "../../globalCssVariables"; +.collectionfreeformview-none, .collectionfreeformview-ease { position: inherit; top: 0; @@ -7,16 +8,15 @@ width: 100%; height: 100%; transform-origin: left top; + border-radius: inherit; +} + +.collectionfreeformview-ease { transition: transform 1s; } .collectionfreeformview-none { - position: inherit; - top: 0; - left: 0; - width: 100%; - height: 100%; - transform-origin: left top; + touch-action: none; } .collectionFreeform-customText { @@ -25,6 +25,9 @@ } .collectionfreeformview-container { + // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. + touch-action: none; + .collectionfreeformview>.jsx-parser { position: inherit; height: 100%; @@ -41,7 +44,6 @@ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; // } - opacity: 0.99; border: 0px solid $light-color-secondary; border-radius: inherit; box-sizing: border-box; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f6240b6f8..9fbfffc82 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,41 +1,46 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, observable } from "mobx"; +import { action, computed, observable, trace, ObservableMap, untracked, reaction, runInAction, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; +import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; import { Id } from "../../../../new_fields/FieldSymbols"; -import { InkField, StrokeData } from "../../../../new_fields/InkField"; +import { InkTool } from "../../../../new_fields/InkField"; import { createSchema, makeInterface } from "../../../../new_fields/Schema"; import { ScriptField } from "../../../../new_fields/ScriptField"; import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types"; import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils"; import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils"; -import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs } from "../../../documents/Documents"; import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; +import { InteractionUtils } from "../../../util/InteractionUtils"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; -import { InkingCanvas } from "../../InkingCanvas"; +import { InkingControl } from "../../InkingControl"; +import { CreatePolyline } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; +import PDFMenu from "../../pdf/PDFMenu"; import { CollectionSubView } from "../CollectionSubView"; import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; +import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas"; +import { computedFn, keepAlive } from "mobx-utils"; +import { TraceMobx } from "../../../../new_fields/util"; import { Timeline } from "../../animationtimeline/Timeline"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -66,12 +71,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { private _lastY: number = 0; private _clusterDistance: number = 75; private _hitCluster = false; + private _layoutComputeReaction: IReactionDisposer | undefined; + private _layoutPoolData = new ObservableMap<string, any>(); + + public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } - @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } + @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); } @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } @@ -184,21 +194,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { NumCast(cd.cluster) : cluster; }, -1); } - tryDragCluster(e: PointerEvent) { - let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - if (cluster !== -1) { - let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); - let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); - let de = new DragManager.DocumentDragData(eles); - de.moveDocument = this.props.moveDocument; - const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); - de.offset = this.getTransform().transformDirection(e.x - left, e.y - top); - de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; - DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, { - handlers: { dragComplete: action(emptyFunction) }, - hideSource: !de.dropAction - }); - return true; + tryDragCluster(e: PointerEvent | TouchEvent) { + let ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); + if (ptsParent) { + let cluster = this.pickCluster(this.getTransform().transformPoint(ptsParent.clientX, ptsParent.clientY)); + if (cluster !== -1) { + let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster); + let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); + let de = new DragManager.DocumentDragData(eles); + de.moveDocument = this.props.moveDocument; + const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0); + de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); + de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; + DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { + handlers: { dragComplete: action(emptyFunction) }, + hideSource: !de.dropAction + }); + return true; + } } return false; @@ -261,94 +274,224 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return clusterColor; } + @observable private _points: { x: number, y: number }[] = []; + @action onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble) return; this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; - if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { + if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); - this._lastX = e.pageX; - this._lastY = e.pageY; + if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = e.pageX; + this._lastY = e.pageY; + } + else { + e.stopPropagation(); + e.preventDefault(); + + if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { + let point = this.getTransform().transformPoint(e.pageX, e.pageY); + this._points.push({ x: point[0], y: point[1] }); + } + } + } + } + + @action + handle1PointerDown = (e: React.TouchEvent) => { + let pt = e.targetTouches.item(0); + if (pt) { + this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; } } + @action onPointerUp = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.TOUCH) && this._points.length <= 1) return; + + if (this._points.length > 1) { + let B = this.svgBounds; + let points = this._points.map(p => ({ x: p.x - B.left, y: p.y - B.top })); + let inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); + this.addDocument(inkDoc); + this._points = []; + } + document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + @action + pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { + // I think it makes sense for the marquee menu to go away when panned. -syip2 + MarqueeOptionsMenu.Instance.fadeOut(true); + + let x = this.Document.panX || 0; + let y = this.Document.panY || 0; + let docs = this.childLayoutPairs.map(pair => pair.layout); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + if (!this.isAnnotationOverlay) { + PDFMenu.Instance.fadeOut(true); + let minx = docs.length ? NumCast(docs[0].x) : 0; + let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let miny = docs.length ? NumCast(docs[0].y) : 0; + let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let ranges = docs.filter(doc => doc).reduce((range, doc) => { + let layoutDoc = Doc.Layout(doc); + let x = NumCast(doc.x); + let xe = x + NumCast(layoutDoc.width); + let y = NumCast(doc.y); + let ye = y + NumCast(layoutDoc.height); + return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], + [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; + }, [[minx, maxx], [miny, maxy]]); + + let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; + let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, + this.props.PanelHeight() / this.zoomScaling() * cscale); + if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; + if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; + if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; + if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + } + this.setPan(x - dx, y - dy); + this._lastX = e.clientX; + this._lastY = e.clientY; } @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble) { - if (this._hitCluster && this.tryDragCluster(e)) { - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - return; + if (InteractionUtils.IsType(e, InteractionUtils.TOUCH)) { + if (this.props.active(true)) { + e.stopPropagation(); } - let x = this.Document.panX || 0; - let y = this.Document.panY || 0; - let docs = this.childLayoutPairs.map(pair => pair.layout); - let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - if (!this.isAnnotationOverlay) { - let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(Doc.Layout(docs[0]).width) + minx : minx; - let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(Doc.Layout(docs[0]).height) + miny : miny; - let ranges = docs.filter(doc => doc).reduce((range, doc) => { - let layoutDoc = Doc.Layout(doc); - let x = NumCast(doc.x); - let xe = x + NumCast(layoutDoc.width); - let y = NumCast(doc.y); - let ye = y + NumCast(layoutDoc.height); - return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], - [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; - }, [[minx, maxx], [miny, maxy]]); - let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField); - if (ink && ink.inkData) { - ink.inkData.forEach((value: StrokeData, key: string) => { - let bounds = InkingCanvas.StrokeRect(value); - ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)]; - ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)]; - }); + return; + } + if (!e.cancelBubble) { + if (InkingControl.Instance.selectedTool === InkTool.None) { + if (this._hitCluster && this.tryDragCluster(e)) { + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + return; } - - let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1; - let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale, - this.props.PanelHeight() / this.zoomScaling() * cscale); - if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2; - if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2; - if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2; - if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2; + this.pan(e); + } + else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { + let point = this.getTransform().transformPoint(e.clientX, e.clientY); + this._points.push({ x: point[0], y: point[1] }); } - this.setPan(x - dx, y - dy); - this._lastX = e.pageX; - this._lastY = e.pageY; e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } - @action - onPointerWheel = (e: React.WheelEvent): void => { - if (this.props.Document.lockedTransform || this.props.Document.inOverlay) return; - if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming + handle1PointerMove = (e: TouchEvent) => { + // panning a workspace + if (!e.cancelBubble) { + let pt = e.targetTouches.item(0); + if (pt) { + if (InkingControl.Instance.selectedTool === InkTool.None) { + if (this._hitCluster && this.tryDragCluster(e)) { + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + return; + } + this.pan(pt); + } + else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { + let point = this.getTransform().transformPoint(pt.clientX, pt.clientY); + this._points.push({ x: point[0], y: point[1] }); + } + } e.stopPropagation(); + e.preventDefault(); } - else if (this.props.active()) { - e.stopPropagation(); - let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1; - if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { - deltaScale = 1 / this.zoomScaling(); + } + + handle2PointersMove = (e: TouchEvent) => { + // pinch zooming + if (!e.cancelBubble) { + let pt1: Touch | null = e.targetTouches.item(0); + let pt2: Touch | null = e.targetTouches.item(1); + if (!pt1 || !pt2) return; + + if (this.prevPoints.size === 2) { + let oldPoint1 = this.prevPoints.get(pt1.identifier); + let oldPoint2 = this.prevPoints.get(pt2.identifier); + if (oldPoint1 && oldPoint2) { + let dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2); + + // if zooming, zoom + if (dir !== 0) { + let d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2)); + let d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2)); + let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + + // calculate the raw delta value + let rawDelta = (dir * (d1 + d2)); + + // this floors and ceils the delta value to prevent jitteriness + let delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 16); + this.zoom(centerX, centerY, delta); + this.prevPoints.set(pt1.identifier, pt1); + this.prevPoints.set(pt2.identifier, pt2); + } + // this is not zooming. derive some form of panning from it. + else { + // use the centerx and centery as the "new mouse position" + let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this.pan({ clientX: centerX, clientY: centerY }); + this._lastX = centerX; + this._lastY = centerY; + } + } } - if (deltaScale < 0) deltaScale = -deltaScale; - let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); - let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + } + e.stopPropagation(); + e.preventDefault(); + } + handle2PointersDown = (e: React.TouchEvent) => { + let pt1: React.Touch | null = e.targetTouches.item(0); + let pt2: React.Touch | null = e.targetTouches.item(1); + if (!pt1 || !pt2) return; + + let centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + let centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + } + + cleanUpInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + document.removeEventListener("touchmove", this.onTouch); + document.removeEventListener("touchend", this.onTouchEnd); + } + + @action + zoom = (pointX: number, pointY: number, deltaY: number): void => { + let deltaScale = deltaY > 0 ? (1 / 1.1) : 1.1; + if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { + deltaScale = 1 / this.zoomScaling(); + } + if (deltaScale < 0) deltaScale = -deltaScale; + let [x, y] = this.getTransform().transformPoint(pointX, pointY); + let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + + if (localTransform.Scale >= 0.15) { let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); @@ -356,6 +499,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action + onPointerWheel = (e: React.WheelEvent): void => { + if (this.props.Document.lockedTransform || this.props.Document.inOverlay) return; + if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming + e.stopPropagation(); + } + else if (this.props.active(true)) { + e.stopPropagation(); + this.zoom(e.clientX, e.clientY, e.deltaY); + } + } + + @action setPan(panX: number, panY: number, panType: string = "None") { if (!this.Document.lockedTransform || this.Document.inOverlay) { this.Document.panTransformType = panType; @@ -469,12 +624,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } { - const script = this.Document.arrangeScript; - const result = script && script.script.run(params, console.log); - const layoutDoc = Doc.Layout(params.doc); - if (result && result.success) { + const result = this.Document.arrangeScript?.script.run(params, console.log); + if (result?.success) { return { ...result, transition: "transform 1s" }; } + const layoutDoc = Doc.Layout(params.doc); return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") }; } @@ -501,64 +655,57 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - lookupLayout = (doc: Doc, dataDoc?: Doc) => { - let data: any = undefined; - let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; - switch (this.Document.freeformLayoutEngine) { - case "pivot": computedElementData = this.doPivotLayout; break; - default: computedElementData = this.doFreeformLayout; break; - } - computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => { - if (key.layout === doc && key.data === dataDoc) { - data = value; - } - }); - return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition }; - } + childDataProvider = computedFn(function childDataProvider(doc: Doc) { return (this as any)._layoutPoolData.get(doc[Id]); }.bind(this)); - @computed - get doPivotLayout() { - return computePivotLayout(this.props.Document, this.childDocs, + doPivotLayout(poolData: ObservableMap<string, any>) { + return computePivotLayout(poolData, this.props.Document, this.childDocs, this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX); } - @computed - get doFreeformLayout() { - let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map(); + doFreeformLayout(poolData: ObservableMap<string, any>) { let layoutDocs = this.childLayoutPairs.map(pair => pair.layout); const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); let state = initResult && initResult.success ? initResult.result.scriptState : undefined; let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : []; this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => { + const data = poolData.get(pair.layout[Id]); const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state }); state = pos.state === undefined ? state : pos.state; - layoutPoolData.set(pair, pos); + if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height || pos.transition !== data.transition) { + runInAction(() => poolData.set(pair.layout[Id], pos)); + } }); - return { map: layoutPoolData, elements: elements }; + return { elements: elements }; } - @computed get doLayoutComputation() { - let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] }; + let computedElementData: { elements: ViewDefResult[] }; switch (this.Document.freeformLayoutEngine) { - case "pivot": computedElementData = this.doPivotLayout; break; - default: computedElementData = this.doFreeformLayout; break; + case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break; + default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break; } this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair => computedElementData.elements.push({ - ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout} + ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider} ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider} jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />, - bounds: this.lookupLayout(pair.layout, pair.data) + bounds: this.childDataProvider(pair.layout) })); return computedElementData; } - @computed.struct get elements() { return this.doLayoutComputation.elements; } - @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } - @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); } + componentDidMount() { + this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; }, + action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)), + { fireImmediately: true, name: "doLayout" }); + } + componentWillUnmount() { + this._layoutComputeReaction && this._layoutComputeReaction(); + } + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } + elementFunc = () => this._layoutElements; @action onCursorMove = (e: React.PointerEvent) => { @@ -603,11 +750,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } analyzeStrokes = async () => { - const extensionDoc = this.extensionDoc; - let data = extensionDoc && Cast(extensionDoc.ink, InkField); - if (data && extensionDoc) { - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData); - } + // CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], data.inkData); } onContextMenu = (e: React.MouseEvent) => { @@ -679,35 +822,75 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ...this.views, ]; } + + @computed get svgBounds() { + let xs = this._points.map(p => p.x); + let ys = this._points.map(p => p.y); + let right = Math.max(...xs); + let left = Math.min(...xs); + let bottom = Math.max(...ys); + let top = Math.min(...ys); + return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top }; + } + + @computed get currentStroke() { + if (this._points.length <= 1) { + return (null); + } + + let B = this.svgBounds; + + return ( + <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)` }}> + {CreatePolyline(this._points, B.left, B.top)} + </svg> + ); + } + + children = () => { + let eles: JSX.Element[] = []; + this.extensionDoc && (eles.push(...this.childViews())); + this.currentStroke && (eles.push(this.currentStroke)); + eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); + return eles; + } render() { + TraceMobx(); // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) - this.Document.fitX = this.contentBounds && this.contentBounds.x; - this.Document.fitY = this.contentBounds && this.contentBounds.y; - this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x); - this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); + // this.Document.fitX = this.contentBounds && this.contentBounds.x; + // this.Document.fitY = this.contentBounds && this.contentBounds.y; + // this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x); + // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y); // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey. // otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document return !this.extensionDoc ? (null) : - <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel} - style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}> + <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, + style={{ height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart}> <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - {!this.extensionDoc ? (null) : - <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} > - {this.childViews} - </InkingCanvas>} - <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> + {this.children} </CollectionFreeFormViewPannableContents> </MarqueeView> <Timeline ref={this._timelineRef} {...this.props} /> - {this.overlayViews} + <CollectionFreeFormOverlayView elements={this.elementFunc} /> </div>; } } +interface CollectionFreeFormOverlayViewProps { + elements: () => ViewDefResult[]; +} + +@observer +class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps>{ + render() { + return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); + } +} + interface CollectionFreeFormViewPannableContentsProps { centeringShiftX: () => number; centeringShiftY: () => number; @@ -715,6 +898,7 @@ interface CollectionFreeFormViewPannableContentsProps { panY: () => number; zoomScaling: () => number; easing: () => boolean; + children: () => JSX.Element[]; } @observer @@ -726,8 +910,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling(); - return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> - {this.props.children} + return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}> + {this.props.children()} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx new file mode 100644 index 000000000..28ddc19d7 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -0,0 +1,46 @@ +import React = require("react"); +import AntimodeMenu from "../../AntimodeMenu"; +import { observer } from "mobx-react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { unimplementedFunction } from "../../../../Utils"; + +@observer +export default class MarqueeOptionsMenu extends AntimodeMenu { + static Instance: MarqueeOptionsMenu; + + public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; + public showMarquee: () => void = unimplementedFunction; + public hideMarquee: () => void = unimplementedFunction; + + constructor(props: Readonly<{}>) { + super(props); + + MarqueeOptionsMenu.Instance = this; + } + + render() { + let buttons = [ + <button + className="antimodeMenu-button" + title="Create a Collection" + onPointerDown={this.createCollection}> + <FontAwesomeIcon icon="object-group" size="lg" /> + </button>, + <button + className="antimodeMenu-button" + title="Summarize Documents" + onPointerDown={this.summarize}> + <FontAwesomeIcon icon="compress-arrows-alt" size="lg" /> + </button>, + <button + className="antimodeMenu-button" + title="Delete Documents" + onPointerDown={this.delete}> + <FontAwesomeIcon icon="trash-alt" size="lg" /> + </button>, + ]; + return this.getElement(buttons); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 04f6ec2ad..d14495626 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -8,6 +8,7 @@ } .marqueeView { overflow: hidden; + pointer-events: inherit; } .marqueeView:focus-within { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 44b6fe030..5ed3fecb5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../../new_fields/Doc"; -import { InkField, StrokeData } from "../../../../new_fields/InkField"; +import { InkField, PointData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { listSpec } from "../../../../new_fields/Schema"; import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField"; @@ -13,12 +13,13 @@ import { Docs } from "../../../documents/Documents"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; -import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionViewType } from "../CollectionView"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; import "./MarqueeView.scss"; import React = require("react"); +import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import InkSelectDecorations from "../../InkSelectDecorations"; import { SubCollectionViewProps } from "../CollectionSubView"; interface MarqueeViewProps { @@ -26,7 +27,7 @@ interface MarqueeViewProps { getTransform: () => Transform; addDocument: (doc: Doc) => boolean; activeDocuments: () => Doc[]; - selectDocuments: (docs: Doc[]) => void; + selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void; removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; @@ -51,13 +52,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @action - cleanupInteractions = (all: boolean = false) => { + cleanupInteractions = (all: boolean = false, hideMarquee: boolean = true) => { if (all) { document.removeEventListener("pointerup", this.onPointerUp, true); document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); - this._visible = false; + if (hideMarquee) { + this._visible = false; + } } @undoBatch @@ -188,15 +191,33 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action onPointerUp = (e: PointerEvent): void => { - if (!this.props.active()) this.props.selectDocuments([this.props.Document]); + if (!this.props.active(true)) this.props.selectDocuments([this.props.Document], []); if (this._visible) { let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document); } - this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]); + // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map(); + // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : []; + let docs = mselect.length ? mselect : [this.props.Document]; + this.props.selectDocuments(docs, []); } - this.cleanupInteractions(true); + if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { + MarqueeOptionsMenu.Instance.createCollection = this.collection; + MarqueeOptionsMenu.Instance.delete = this.delete; + MarqueeOptionsMenu.Instance.summarize = this.summary; + MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; + MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; + MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + } + this.cleanupInteractions(true, this._commandExecuted); + + let hideMarquee = () => { + this.hideMarquee(); + MarqueeOptionsMenu.Instance.fadeOut(true); + document.removeEventListener("pointerdown", hideMarquee); + }; + document.addEventListener("pointerdown", hideMarquee); if (e.altKey) { e.preventDefault(); @@ -246,6 +267,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }; } + get inkDoc() { + return this.props.extensionDoc; + } + get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored. return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField); } @@ -254,6 +279,120 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.extensionDoc && (this.props.extensionDoc.ink = value); } + @action + showMarquee = () => { + this._visible = true; + } + + @action + hideMarquee = () => { + this._visible = false; + } + + @action + delete = () => { + this.marqueeSelect(false).map(d => this.props.removeDocument(d)); + if (this.ink) { + // this.marqueeInkDelete(this.ink.inkData); + } + SelectionManager.DeselectAll(); + this.cleanupInteractions(false); + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + getCollection = (selected: Doc[]) => { + let bounds = this.Bounds; + let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", + "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; + let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string")); + if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette); + let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]); + let usedPaletted = new Map<string, number>(); + [...this.props.activeDocuments(), this.props.Document].map(child => { + let bg = StrCast(Doc.Layout(child).backgroundColor); + if (palette.indexOf(bg) !== -1) { + palette.splice(palette.indexOf(bg), 1); + if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); + else usedPaletted.set(bg, 1); + } + }); + usedPaletted.delete("#f1efeb"); + usedPaletted.delete("white"); + usedPaletted.delete("rgba(255,255,255,1)"); + let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); + let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; + let inkData = this.ink ? this.ink.inkData : undefined; + let newCollection = Docs.Create.FreeformDocument(selected, { + x: bounds.left, + y: bounds.top, + panX: 0, + panY: 0, + backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, + width: bounds.width, + height: bounds.height, + title: "a nested collection", + }); + let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); + // dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; + // this.marqueeInkDelete(inkData); + this.hideMarquee(); + return newCollection; + } + + @action + collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { + let bounds = this.Bounds; + let selected = this.marqueeSelect(false); + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.displayTimecode = undefined; + return d; + }); + } + let newCollection = this.getCollection(selected); + this.props.addDocument(newCollection); + this.props.selectDocuments([newCollection], []); + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + @action + summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { + let bounds = this.Bounds; + let selected = this.marqueeSelect(false); + let newCollection = this.getCollection(selected); + + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; + }); + newCollection.chromeStatus = "disabled"; + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); + newCollection.x = bounds.left + bounds.width; + Doc.GetProto(newCollection).summaryDoc = summary; + Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); + if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); + container.viewType = CollectionViewType.Stacking; + container.autoHeight = true; + Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" + this.props.addLiveTextDocument(container); + } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them + Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" + this.props.addLiveTextDocument(summary); + } + MarqueeOptionsMenu.Instance.fadeOut(true); + } + @undoBatch @action marqueeCommand = async (e: KeyboardEvent) => { @@ -264,12 +403,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._commandExecuted = true; e.stopPropagation(); (e as any).propagationIsStopped = true; - this.marqueeSelect(false).map(d => this.props.removeDocument(d)); - if (this.ink) { - this.marqueeInkDelete(this.ink.inkData); - } - SelectionManager.DeselectAll(); - this.cleanupInteractions(false); + this.delete(); e.stopPropagation(); } if (e.key === "c" || e.key === "s" || e.key === "S") { @@ -277,117 +411,55 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.stopPropagation(); e.preventDefault(); (e as any).propagationIsStopped = true; - let bounds = this.Bounds; - let selected = this.marqueeSelect(false); if (e.key === "c") { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.displayTimecode = undefined; - return d; - }); + this.collection(e); } - let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)", - "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",]; - let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string")); - if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette); - let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]); - let usedPaletted = new Map<string, number>(); - [...this.props.activeDocuments(), this.props.Document].map(child => { - let bg = StrCast(Doc.Layout(child).backgroundColor); - if (palette.indexOf(bg) !== -1) { - palette.splice(palette.indexOf(bg), 1); - if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1); - else usedPaletted.set(bg, 1); - } - }); - usedPaletted.delete("#f1efeb"); - usedPaletted.delete("white"); - usedPaletted.delete("rgba(255,255,255,1)"); - let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0); - let chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0]; - let inkData = this.ink ? this.ink.inkData : undefined; - let newCollection = Docs.Create.FreeformDocument(selected, { - x: bounds.left, - y: bounds.top, - panX: 0, - panY: 0, - backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor, - width: bounds.width, - height: bounds.height, - title: "a nested collection", - }); - let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data"); - dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined; - this.marqueeInkDelete(inkData); if (e.key === "s" || e.key === "S") { - selected.map(d => { - this.props.removeDocument(d); - d.x = NumCast(d.x) - bounds.left - bounds.width / 2; - d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; - return d; - }); - newCollection.chromeStatus = "disabled"; - let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); - Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]); - newCollection.x = bounds.left + bounds.width; - Doc.GetProto(newCollection).summaryDoc = summary; - Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`); - if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view. - let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" }); - container.viewType = CollectionViewType.Stacking; - container.autoHeight = true; - Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight" - this.props.addLiveTextDocument(container); - } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them - Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight" - this.props.addLiveTextDocument(summary); - } - } - else { - this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection]); + this.summary(e); } this.cleanupInteractions(false); } } - @action - marqueeInkSelect(ink: Map<any, any>) { - let idata = new Map(); - let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin. - let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2); - ink.forEach((value: StrokeData, key: string, map: any) => { - if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) { - idata.set(key, - { - pathData: value.pathData.map(val => ({ x: val.x + centerShiftX, y: val.y + centerShiftY })), - color: value.color, - width: value.width, - tool: value.tool, - page: -1 - }); - } - }); - return idata; - } + // @action + // marqueeInkSelect(ink: Map<any, any>) { + // let idata = new Map(); + // let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin. + // let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2); + // ink.forEach((value: PointData, key: string, map: any) => { + // if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) { + // // let transform = this.props.container.props.ScreenToLocalTransform().scale(this.props.container.props.ContentScaling()); + // idata.set(key, + // { + // pathData: value.pathData.map(val => { + // let tVal = this.props.getTransform().inverse().transformPoint(val.x, val.y); + // return { x: tVal[0], y: tVal[1] }; + // // return { x: val.x + centerShiftX, y: val.y + centerShiftY } + // }), + // color: value.color, + // width: value.width, + // tool: value.tool, + // page: -1 + // }); + // } + // }); + // // InkSelectDecorations.Instance.SetSelected(idata); + // return idata; + // } - @action - marqueeInkDelete(ink?: Map<any, any>) { - // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB. - // ink.forEach((value: StrokeData, key: string, map: any) => - // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key)); + // @action + // marqueeInkDelete(ink?: Map<any, any>) { + // // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB. + // // ink.forEach((value: StrokeData, key: string, map: any) => + // // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key)); - if (ink) { - let idata = new Map(); - ink.forEach((value: StrokeData, key: string, map: any) => - !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); - this.ink = new InkField(idata); - } - } + // if (ink) { + // let idata = new Map(); + // ink.forEach((value: PointData, key: string, map: any) => + // !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value)); + // this.ink = new InkField(idata); + // } + // } marqueeSelect(selectBackgrounds: boolean = true) { let selRect = this.Bounds; @@ -438,8 +510,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque get marqueeDiv() { let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + /** + * @RE - The commented out span below + * This contains the "C for collection, ..." text on marquees. + * Commented out by syip2 when the marquee menu was added. + */ return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} > - <span className="marquee-legend" /> + {/* <span className="marquee-legend" /> */} </div>; } diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss index e8a3d1479..7c3825978 100644 --- a/src/client/views/nodes/ButtonBox.scss +++ b/src/client/views/nodes/ButtonBox.scss @@ -3,7 +3,7 @@ height: 100%; pointer-events: all; border-radius: inherit; - display:flex; + display: flex; flex-direction: column; } @@ -15,19 +15,22 @@ display: table; overflow: hidden; text-overflow: ellipsis; + letter-spacing: 2px; + text-transform: uppercase; } + .buttonBox-mainButtonCenter { height: 100%; - display:table-cell; + display: table-cell; vertical-align: middle; } .buttonBox-params { - display:flex; - flex-direction: row; + display: flex; + flex-direction: row; } .buttonBox-missingParam { - width:100%; + width: 100%; background: lightgray; }
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index beb2b30fd..659ba154a 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -79,7 +79,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt return ( <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}> - <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black" }} > + <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black", fontSize: this.Document.fontSize }} > <div className="buttonBox-mainButtonCenter"> {(this.Document.text || this.Document.title)} </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss index af9232c2f..da287649e 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss @@ -2,6 +2,7 @@ transform-origin: left top; position: absolute; background-color: transparent; - top:0; - left:0; + touch-action: manipulation; + top: 0; + left: 0; }
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a035bdc3d..c85b59488 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -10,9 +10,10 @@ import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); import { PositionDocument } from "../../../new_fields/documentSchemas"; +import { TraceMobx } from "../../../new_fields/util"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { - dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; + dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined; x?: number; y?: number; width?: number; @@ -24,12 +25,16 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { @observer export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) { _disposer: IReactionDisposer | undefined = undefined; + get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; } get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); } get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); } get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); } - get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); } - @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; } + get height() { + let hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); + return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt; + } + @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; } @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); } @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); } @@ -52,11 +57,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this._disposer && this._disposer(); } componentDidMount() { - this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating], - () => { - const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined; - this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]); - }, { fireImmediately: true }); + this._disposer = reaction(() => this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined, + target => this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]), + { fireImmediately: true }); } contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1; @@ -84,7 +87,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight(); render() { - trace(); + TraceMobx(); return <div className="collectionFreeFormDocumentView-container" style={{ boxShadow: @@ -95,7 +98,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF StrCast(this.layoutDoc.boxShadow, ""), borderRadius: this.borderRounding(), transform: this.transform, - transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), + transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition), width: this.width, height: this.height, zIndex: this.Document.zIndex || 0, diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index fda6d64f4..40674b034 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -38,7 +38,9 @@ export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocume render() { return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} - onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}> + onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} + style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > + <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} /> </div>; } diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx index c8255b6fe..573a55710 100644 --- a/src/client/views/nodes/ContentFittingDocumentView.tsx +++ b/src/client/views/nodes/ContentFittingDocumentView.tsx @@ -32,7 +32,7 @@ interface ContentFittingDocumentViewProps { addDocument: (document: Doc) => boolean; moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; removeDocument: (document: Doc) => boolean; - active: () => boolean; + active: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 779d25cdd..b9b84d5ce 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -30,6 +30,7 @@ import { DocuLinkBox } from "./DocuLinkBox"; import { PresElementBox } from "../presentationview/PresElementBox"; import { VideoBox } from "./VideoBox"; import { WebBox } from "./WebBox"; +import { InkingStroke } from "../InkingStroke"; import React = require("react"); const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -49,7 +50,7 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; @observer export class DocumentContentsView extends React.Component<DocumentViewProps & { - isSelected: () => boolean, + isSelected: (outsideReaction: boolean) => boolean, select: (ctrl: boolean) => void, onClick?: ScriptField, layoutKey: string, @@ -97,7 +98,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, ColorBox, DocuLinkBox + PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox, + ColorBox, DocuLinkBox, InkingStroke }} bindings={this.CreateBindings()} jsx={this.layout} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 65df86d27..dfb84ed5c 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,13 +1,14 @@ @import "../globalCssVariables"; -.documentView-node, .documentView-node-topmost { +.documentView-node, +.documentView-node-topmost { position: inherit; top: 0; - left:0; + left: 0; border-radius: inherit; - transition : outline .3s linear; + transition: outline .3s linear; cursor: grab; - + // background: $light-color; //overflow: hidden; transform-origin: left top; @@ -37,44 +38,48 @@ position: absolute; transform-origin: top left; width: 100%; - height: 100%; + height: 100%; } + .documentView-styleWrapper { - position: absolute; + position: absolute; display: inline-block; - width:100%; - height:100%; + width: 100%; + height: 100%; pointer-events: none; .documentView-styleContentWrapper { - width:100%; + width: 100%; display: inline-block; position: absolute; } + .documentView-titleWrapper { - overflow:hidden; - color: white; - transform-origin: top left; - top: 0; + overflow: hidden; + color: white; + transform-origin: top left; + top: 0; + width: 100%; height: 25; background: rgba(0, 0, 0, .4); - padding: 4px; - text-align: center; - text-overflow: ellipsis; + padding: 4px; + text-align: center; + text-overflow: ellipsis; white-space: pre; } .documentView-searchHighlight { - position: absolute; - background: yellow; + position: absolute; + background: yellow; bottom: -20px; border-radius: 5px; - transform-origin: bottom left; + transform-origin: bottom left; } .documentView-captionWrapper { - position: absolute; - bottom: 0; + position: absolute; + bottom: 0; + width: 100%; transform-origin: bottom left; } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7132f181e..467fd4b32 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -11,13 +11,12 @@ import { ScriptField } from '../../../new_fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types"; import { ImageField } from '../../../new_fields/URLField'; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils"; +import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; import { ClientUtils } from '../../util/ClientUtils'; -import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; import { LinkManager } from '../../util/LinkManager'; @@ -41,6 +40,10 @@ import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); +import { InteractionUtils } from '../../util/InteractionUtils'; +import { InkingControl } from '../InkingControl'; +import { InkTool } from '../../../new_fields/InkField'; +import { TraceMobx } from '../../../new_fields/util'; library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight, fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale, @@ -64,7 +67,7 @@ export interface DocumentViewProps { PanelWidth: () => number; PanelHeight: () => number; focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; - parentActive: () => boolean; + parentActive: (outsideReaction: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; @@ -89,8 +92,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; + public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } - @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @computed get topMost() { return this.props.renderDepth === 0; } @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; } @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; } @@ -208,14 +212,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu document.addEventListener("pointerup", this.onPointerUp); if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } + onPointerMove = (e: PointerEvent): void => { if ((e as any).formattedHandled) { e.stopPropagation(); return; } if (e.cancelBubble && this.active) { document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) { if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) { + if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCH))) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); @@ -225,6 +230,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.preventDefault(); } } + onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -378,17 +384,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.Document.lockedTransform = this.Document.lockedTransform ? undefined : true; } - listen = async () => { - Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({ - continuous: { indefinite: true }, - interimHandler: (results: string) => { - DictationOverlay.Instance.dictationSuccess = true; - DictationOverlay.Instance.dictatedPhrase = results; - DictationOverlay.Instance.isListening = { interim: true }; - } - }); - } - @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { e.persist(); @@ -410,14 +405,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - if (Cast(this.props.Document.data, ImageField)) { - cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); - } - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); - } let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -435,10 +422,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); - ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + if (this.Document.onDragStart) { + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined }); + ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" }); + } let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; @@ -459,18 +448,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); + + let more = ContextMenu.Instance.findByDescription("More..."); + let moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : []; + if (!ClientUtils.RELEASE) { // let copies: ContextMenuProps[] = []; - cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } - let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); - let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; - analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); - !existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" }); - cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! - cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); - cm.addItem({ + if (Cast(this.props.Document.data, ImageField)) { + moreItems.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + moreItems.push({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! + moreItems.push({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); + moreItems.push({ description: "Download document", icon: "download", event: async () => console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } @@ -482,8 +479,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu // a.click(); }); - cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); - cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + moreItems.push({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); + moreItems.push({ description: "Delete", event: this.deleteClicked, icon: "trash" }); + moreItems.push({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); runInAction(() => { if (!ClientUtils.RELEASE) { let setWriteMode = (mode: DocServer.WriteMode) => { @@ -506,7 +505,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" }); aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" }); cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); - cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); } }); runInAction(() => { @@ -521,7 +519,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.stopPropagation(); } ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - if (!SelectionManager.IsSelected(this)) { + if (!SelectionManager.IsSelected(this, true)) { SelectionManager.SelectDoc(this, false); } }); @@ -533,7 +531,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); - isSelected = () => SelectionManager.IsSelected(this); + isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; chromeHeight = () => { @@ -542,8 +540,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return (showTitle ? 25 : 0) + 1; } + @computed get finalLayoutKey() { return this.props.layoutKey || "layout"; } childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { + TraceMobx(); return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} Document={this.props.Document} @@ -572,12 +572,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - layoutKey={this.props.layoutKey || "layout"} + layoutKey={this.finalLayoutKey} DataDoc={this.props.DataDoc} />); } linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document); - // used to decide whether a link document should be created or not. + // used to decide whether a link document should be created or not. // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here. // would be good to generalize this some way. isNonTemporalLink = (linkDoc: Doc) => { @@ -587,16 +587,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get innards() { + TraceMobx(); const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; const searchHighlight = (!this.Document.searchFields ? (null) : - <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <div className="documentView-searchHighlight"> {this.Document.searchFields} </div>); const captionView = (!showCaption ? (null) : - <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <div className="documentView-captionWrapper"> <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} @@ -605,7 +606,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu </div>); const titleView = (!showTitle ? (null) : <div className="documentView-titleWrapper" style={{ - width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})`, position: showTextTitle ? "relative" : "absolute", pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", }}> @@ -617,9 +617,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu /> </div>); return <> - {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) => - <div className="documentView-docuLinkWrapper" key={`${d[Id]}`} style={{ transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}> - <DocumentView {...this.props} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} /> + {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) => + <div className="documentView-docuLinkWrapper" key={`${d[Id]}`}> + <DocumentView {...this.props} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} /> </div>)} {!showTitle && !showCaption ? this.Document.searchFields ? @@ -641,43 +641,50 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } </>; } + @computed get ignorePointerEvents() { + return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None); + } + render() { if (!this.props.Document) return (null); - trace(); - const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; const colorSet = this.setsLayoutProp("backgroundColor"); const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; - const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ? + const backgroundColor = (clusterCol && !colorSet) ? this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); - const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%"; - const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; - const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + const localScale = fullDegree; - let animheight = animDims ? animDims[1] : nativeHeight; - let animwidth = animDims ? animDims[0] : nativeWidth; + const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined; + let animheight = animDims ? animDims[1] : "100%"; + let animwidth = animDims ? animDims[0] : "100%"; const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; - const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; + const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear; return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)} + onPointerEnter={e => { + console.log("Brush" + this.props.Document.title); + Doc.BrushDoc(this.props.Document); + }} onPointerLeave={e => { + console.log("UnBrush" + this.props.Document.title); + Doc.UnBrushDoc(this.props.Document); + + }} style={{ - transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), - pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", + transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition), + pointerEvents: this.ignorePointerEvents ? "none" : "all", color: StrCast(this.Document.color), outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor, width: animwidth, height: animheight, - transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} > {this.innards} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 5108954bb..c93746773 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -31,7 +31,7 @@ export interface FieldViewProps { Document: Doc; DataDoc?: Doc; onClick?: ScriptField; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; addDocument?: (document: Doc) => boolean; @@ -40,7 +40,7 @@ export interface FieldViewProps { removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - active: () => boolean; + active: (outsideReaction?: boolean) => boolean; whenActiveChanged: (isActive: boolean) => void; focus: (doc: Doc) => void; PanelWidth: () => number; diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 75d093fcb..905601ce3 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -5,9 +5,9 @@ border-radius: inherit; background: black; border-radius: 100%; + transform-origin: top left; svg { - margin:18%; - width:65% !important; - height:65%; + width:95% !important; + height:95%; } } diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 83ecc4657..960b55e3e 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -39,7 +39,10 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document); let referenceLayout = Doc.Layout(referenceDoc); return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} - style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}> + style={{ + background: StrCast(referenceLayout.backgroundColor), + boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined + }}> <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" /> </button>; } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index b497b12b4..77cdd3d42 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -27,6 +27,9 @@ pointer-events: all; overflow-y: auto; max-height: 100%; + display: flex; + flex-direction: row; + .formattedTextBox-dictation { height: 20px; width: 20px; @@ -39,14 +42,41 @@ .collectionfreeformview-container { position: relative; } + .formattedTextBox-outer { position: relative; overflow: auto; display: inline-block; - padding: 10px 10px; width: 100%; height: 100%; } +.formattedTextBox-sidebar-handle { + position: absolute; + top: calc(50% - 17.5px); + width: 10px; + height: 35px; + background: lightgray; + border-radius: 20px; +} +.formattedTextBox-cont > .formattedTextBox-sidebar-handle { + right: 0; + left: unset; +} +.formattedTextBox-sidebar, .formattedTextBox-sidebar-inking { + border-left: dashed 1px black; + height: 100%; + display: inline-block; + position: absolute; + right: 0; + > .formattedTextBox-sidebar-handle { + right:unset; + left:-5; + } +} + +.formattedTextBox-sidebar-inking { + pointer-events: all; +} .formattedTextBox-inner-rounded { height: 70%; @@ -57,8 +87,8 @@ left: 10%; } -.formattedTextBox-inner-rounded , -.formattedTextBox-inner { +.formattedTextBox-inner-rounded, +.formattedTextBox-inner { padding: 10px 10px; height: 100%; } @@ -85,19 +115,20 @@ } .userMarkOpen { - background: rgba(255, 255, 0, 0.267); + background: rgba(255, 255, 0, 0.267); display: inline; } + .userMark { - background: rgba(255, 255, 0, 0.267); + background: rgba(255, 255, 0, 0.267); font-size: 2px; display: inline-grid; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - width:10px; - min-height:10px; - text-align:center; + width: 10px; + min-height: 10px; + text-align: center; align-content: center; } @@ -105,8 +136,9 @@ footnote { display: inline-block; position: relative; cursor: pointer; + div { - padding : 0 !important; + padding: 0 !important; } } @@ -119,7 +151,7 @@ footnote::after { .ProseMirror { counter-reset: prosemirror-footnote; - } +} .footnote-tooltip { cursor: auto; @@ -138,6 +170,7 @@ footnote::after { .prosemirror-attribution { font-size: 8px; } + .footnote-tooltip::before { border: 5px solid silver; border-top-width: 0px; @@ -152,45 +185,247 @@ footnote::after { } .formattedTextBox-summarizer { - opacity :0.5; + opacity: 0.5; position: relative; - width:40px; - height:20px; + width: 40px; + height: 20px; } -.formattedTextBox-summarizer::after{ - content: "←" ; + +.formattedTextBox-summarizer::after { + content: "←"; } .formattedTextBox-summarizer-collapsed { - opacity :0.5; + opacity: 0.5; position: relative; - width:40px; - height:20px; + width: 40px; + height: 20px; } + .formattedTextBox-summarizer-collapsed::after { content: "..."; } .ProseMirror { -ol { counter-reset: deci1 0; padding-left: 0px; } -.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } } -.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } } -.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } } -.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } } -.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18; } -.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; } -.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; } -.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;} -.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35} -.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35} -.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40} -.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40} -.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45} -.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50} -.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 } -.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 } -.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35} -} + touch-action: none; + + ol { + counter-reset: deci1 0; + padding-left: 0px; + } + + .decimal1-ol { + counter-reset: deci1; + + p { + display: inline + } + + ; + font-size: 24; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal2-ol { + counter-reset: deci2; + + p { + display: inline + } + + ; + font-size: 18; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal3-ol { + counter-reset: deci3; + + p { + display: inline + } + + ; + font-size: 14; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal4-ol { + counter-reset: deci4; + + p { + display: inline + } + + ; + font-size: 10; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal5-ol { + counter-reset: deci5; + + p { + display: inline + } + + ; + font-size: 10; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal6-ol { + counter-reset: deci6; + + p { + display: inline + } + + ; + font-size: 10; + + ul, + ol { + padding-left: 30px; + } + } + + .decimal7-ol { + counter-reset: deci7; + + p { + display: inline + } + + ; + font-size: 10; + + ul, + ol { + padding-left: 30px; + } + } + + .upper-alpha-ol { + counter-reset: ualph; + + p { + display: inline + } + + ; + font-size: 18; + } + + .lower-roman-ol { + counter-reset: lroman; + + p { + display: inline + } + + ; + font-size: 14; + } + + .lower-alpha-ol { + counter-reset: lalpha; + + p { + display: inline + } + + ; + font-size: 10; + } + + .decimal1:before { + content: counter(deci1) ") "; + counter-increment: deci1; + display: inline-block; + min-width: 30; + } + + .decimal2:before { + content: counter(deci1) "."counter(deci2) ") "; + counter-increment: deci2; + display: inline-block; + min-width: 35 + } + + .decimal3:before { + content: counter(deci1) "."counter(deci2) "."counter(deci3) ") "; + counter-increment: deci3; + display: inline-block; + min-width: 35 + } + + .decimal4:before { + content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ") "; + counter-increment: deci4; + display: inline-block; + min-width: 40 + } + + .decimal5:before { + content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ") "; + counter-increment: deci5; + display: inline-block; + min-width: 40 + } + + .decimal6:before { + content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ") "; + counter-increment: deci6; + display: inline-block; + min-width: 45 + } + + .decimal7:before { + content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ") "; + counter-increment: deci7; + display: inline-block; + min-width: 50 + } + + .upper-alpha:before { + content: counter(deci1) "."counter(ualph, upper-alpha) ") "; + counter-increment: ualph; + display: inline-block; + min-width: 35 + } + + .lower-roman:before { + content: counter(deci1) "."counter(ualph, upper-alpha) "."counter(lroman, lower-roman) ") "; + counter-increment: lroman; + display: inline-block; + min-width: 50 + } + + .lower-alpha:before { + content: counter(deci1) "."counter(ualph, upper-alpha) "."counter(lroman, lower-roman) "."counter(lalpha, lower-alpha) ") "; + counter-increment: lalpha; + display: inline-block; + min-width: 35 + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 35212b732..9910c9ecd 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -46,6 +46,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { AudioBox } from './AudioBox'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { InkTool } from '../../../new_fields/InkField'; +import { TraceMobx } from '../../../new_fields/util'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -357,9 +359,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } + toggleSidebar = () => this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"; + specificContextMenu = (e: React.MouseEvent): void => { let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"; }, icon: "expand-arrows-alt" }); + funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" }); funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => funcs.push({ @@ -838,7 +842,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (this.props.onClick && e.button === 0) { e.preventDefault(); } - if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { @@ -853,7 +857,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } (e.nativeEvent as any).formattedHandled = true; - if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { + if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { e.stopPropagation(); } } @@ -866,7 +870,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time - if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { e.stopPropagation(); } } @@ -877,7 +881,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & onClick = (e: React.MouseEvent): void => { if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } (e.nativeEvent as any).formattedHandled = true; - // if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { + // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) { // let href = (e.target as any).href; // let location: string; // if ((e.target as any).attributes.location) { @@ -917,7 +921,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) { clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - if (this.props.isSelected() && offsetX < 40) { + if (this.props.isSelected(true) && offsetX < 40) { let pos = this._editorView!.posAtCoords({ left: x, top: y }); if (pos && pos.pos > 0) { let node = this._editorView!.state.doc.nodeAt(pos.pos); @@ -996,13 +1000,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); } + if (this._recording) { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 250); + } } @action tryUpdateHeight() { - let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 && + const scrollHeight = this._ref.current?.scrollHeight; + if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.layoutDoc.height, 0); @@ -1011,11 +1018,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & } } - @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidth, "0%"); } + @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); } @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); } @computed get annotationsKey() { return "annotations"; } render() { - trace(); + TraceMobx(); let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { @@ -1049,29 +1056,33 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }}> <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> </div> - {this.sidebarWidthPercent === "0%" ? (null) : <div style={{ borderLeft: "solid 1px black", width: `${this.sidebarWidthPercent}`, height: "100%", display: "inline-block" }}> - <CollectionFreeFormView {...this.props} - PanelHeight={() => this.props.PanelHeight()} - PanelWidth={() => this.sidebarWidth} - annotationsKey={this.annotationsKey} - isAnnotationOverlay={true} - focus={this.props.focus} - isSelected={this.props.isSelected} - select={emptyFunction} - active={this.active} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} - ruleProvider={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - chromeCollapsed={true}> - </CollectionFreeFormView> - </div>} + {this.sidebarWidthPercent === "0%" ? + <div className="formattedTextBox-sidebar-handle" onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} /> : + <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}> + <CollectionFreeFormView {...this.props} + PanelHeight={() => this.props.PanelHeight()} + PanelWidth={() => this.sidebarWidth} + annotationsKey={this.annotationsKey} + isAnnotationOverlay={true} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + </CollectionFreeFormView> + <div className="formattedTextBox-sidebar-handle" onPointerDown={e => e.stopPropagation()} onClick={e => this.toggleSidebar()} /> + </div>} <div className="formattedTextBox-dictation" onClick={e => { this._recording ? this.stopDictation(true) : this.recordDictation(); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 57c024bbf..ba4ef8879 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -11,10 +11,12 @@ } .imageBox-container { + pointer-events: all; border-radius: inherit; width:100%; height:100%; position: absolute; + transform-origin: top left; } .imageBox-cont-interactive { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 212c99f9d..f21ce3bf2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -27,6 +27,7 @@ import React = require("react"); import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; +import { TraceMobx } from '../../../new_fields/util'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); @@ -268,6 +269,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum } @computed get content() { + TraceMobx(); const extensionDoc = this.extensionDoc; if (!extensionDoc) return (null); // let transform = this.props.ScreenToLocalTransform().inverse(); @@ -290,7 +292,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum if (field instanceof ImageField) paths = [this.choosePath(field.url)]; paths.push(...altpaths); // } - let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive"; + let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive"; let rotation = NumCast(this.Document.rotation, 0); let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; @@ -330,8 +332,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum </div>); } + contentFunc = () => [this.content]; render() { - return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}> + return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu} + style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} @@ -340,7 +344,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum focus={this.props.focus} isSelected={this.props.isSelected} select={emptyFunction} - active={this.active} + active={this.annotationsActive} ContentScaling={returnOne} whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} @@ -352,7 +356,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> - {() => [this.content]} + {this.contentFunc} </CollectionFreeFormView> </div >); } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 35e9e4862..aa6e135fe 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -95,7 +95,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && this.props.isSelected()) { + if (e.buttons === 1 && this.props.isSelected(true)) { e.stopPropagation(); } } diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 2d92c9581..46af63a7d 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,3 +1,11 @@ +.pdfBox-ui { + position: absolute; + width: 100%; + height:100%; + z-index: 1; + pointer-events: none; +} + .pdfBox-cont, .pdfBox-cont-interactive { display: inline-block; @@ -8,6 +16,7 @@ position:absolute; cursor:auto; transform-origin: top left; + z-index: 0; } .pdfBox-title-outer { @@ -26,10 +35,10 @@ color:lightgray; margin-top: auto; margin-bottom: auto; - transform-origin: 42% -18%; + transform-origin: 42% 15%; width: 100%; transform: rotate(55deg); - font-size: 144; + font-size: 72; padding: 5%; overflow: hidden; display: inline-block; @@ -48,7 +57,6 @@ } .pdfViewer-text { .textLayer { - will-change: transform; span { user-select: none; } @@ -60,7 +68,6 @@ pointer-events: all; .pdfViewer-text { .textLayer { - will-change: transform; span { user-select: text; } @@ -135,14 +142,13 @@ .pdfBox-overlayCont { position: absolute; - width: 100%; - height: 40px; + width: calc(100% - 40px); + height: 30px; background: #121721; bottom: 0; display: flex; justify-content: center; align-items: center; - padding: 20px; overflow: hidden; transition: left .5s; pointer-events: all; @@ -172,28 +178,24 @@ border-right: 15px solid #121721; transition: all 0.5s; } +} - .pdfBox-overlayButton-iconCont, - .pdfBox-nextIcon, - .pdfBox-prevIcon { - background: #121721; - height: 30px; - width: 70px; - display: flex; - justify-content: center; - align-items: center; - margin-left: -2px; - border-radius: 3px; - } +.pdfBox-overlayButton-iconCont, +.pdfBox-nextIcon, +.pdfBox-prevIcon { + padding: 0; + background: #121721; + height: 30px; + width: 25px; + display: inline-block; + position: relative; + justify-content: center; + align-items: center; + margin-left: -2px; + border-radius: 3px; + pointer-events: all; } .pdfBox-overlayButton:hover { background: none; } - -.pdfBox-nextIcon { - left: 20; top: 5; height: 30px; position: absolute; -} -.pdfBox-prevIcon { - left: 50; top: 5; height: 30px; position: absolute; -} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8e0515f8a..23512543a 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -62,7 +62,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> this._selectReactionDisposer = reaction(() => this.props.isSelected(), () => { document.removeEventListener("keydown", this.onKeyDown); - this.props.isSelected() && document.addEventListener("keydown", this.onKeyDown); + this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown); }, { fireImmediately: true }); } @@ -111,26 +111,24 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value; private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; - whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive); + whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; }; searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; settingsPanel() { let pageBtns = <> <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back" - onPointerDown={e => e.stopPropagation()} onClick={this.backPage} - style={{ left: 50, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} style={{ left: 45, top: 5 }}> <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" /> </button> <button className="pdfBox-overlayButton-iconCont" key="fwd" title="Page Forward" - onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} - style={{ left: 80, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}> + onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} style={{ left: 45, top: 5 }}> <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" /> </button> </>; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} - onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}> + onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title="Open Search Bar" /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> @@ -143,14 +141,14 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" /> </button> </div> - <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 8, right: 0 }}> + <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 0, right: 0 }}> <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div> </button> <input value={`${(this.Document.curPage || 1)}`} onChange={e => this.gotoPage(Number(e.currentTarget.value))} - style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }} + style={{ left: 5, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}> @@ -199,33 +197,24 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument> let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); return <div className="pdfBox-title-outer" > <div className={classname} > - <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong> + <strong className="pdfBox-title" >{this.props.Document.title}</strong> </div> </div>; } + isChildActive = (outsideReaction?: boolean) => this._isChildActive; @computed get renderPdfView() { - trace(); const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField); - let classname = "pdfBox-cont" + (this.active() ? "-interactive" : ""); - return <div className={classname} style={{ - width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, - height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined, - transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})` - }} onContextMenu={this.specificContextMenu} onPointerDown={e => { - let hit = document.elementFromPoint(e.clientX, e.clientY); - if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation - e.button === 0 && e.stopPropagation(); - } - }}> + return <div className={"pdfBox-cont"} onContextMenu={this.specificContextMenu}> <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} addDocTab={this.props.addDocTab} focus={this.props.focus} pinToPres={this.props.pinToPres} addDocument={this.addDocument} + Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged} + isChildActive={this.isChildActive} fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} /> {this.settingsPanel()} </div>; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 5829c1bd9..0a4c650a8 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,5 +1,6 @@ .videoBox-container { pointer-events: all; + transform-origin: top left; .inkingCanvas-paths-markers { opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 1e48000b6..c43c9d6c4 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -251,7 +251,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum this._youtubeReactionDisposer && this._youtubeReactionDisposer(); this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0)); this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => { - let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting; + let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected(true) && !DocumentDecorations.Instance.Interacting; iframe.style.pointerEvents = interactive ? "all" : "none"; }, { fireImmediately: true }); }; @@ -335,8 +335,10 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum return this.addDocument(doc); } + contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; render() { - return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}> + return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu} + style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} @@ -345,7 +347,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum isSelected={this.props.isSelected} isAnnotationOverlay={true} select={emptyFunction} - active={this.active} + active={this.annotationsActive} ContentScaling={returnOne} whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} @@ -357,7 +359,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> - {() => [this.youtubeVideoId ? this.youtubeContent : this.content]} + {this.contentFunc} </CollectionFreeFormView> {this.uIButtons} </div >); diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss index 44e075153..3c08ba80d 100644 --- a/src/client/views/pdf/PDFMenu.scss +++ b/src/client/views/pdf/PDFMenu.scss @@ -1,36 +1,6 @@ -.pdfMenu-cont { - position: absolute; - z-index: 10000; - height: 35px; - background: #323232; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); - border-radius: 0px 6px 6px 6px; - overflow: hidden; - display: flex; - - .pdfMenu-button { - background-color: transparent; - width: 35px; - height: 35px; - } - - .pdfMenu-button:hover { - background-color: #d4d4d4; - } - - .pdfMenu-dragger { - height: 100%; - transition: width .2s; - background-image: url("https://logodix.com/logo/1020374.png"); - background-size: 90% 100%; - background-repeat: no-repeat; - background-position: left center; - } - - .pdfMenu-addTag { - display: grid; - width: 200px; - padding: 5px; - grid-template-columns: 90px 20px 90px; - } +.pdfMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 517a99a68..c64741769 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -3,39 +3,30 @@ import "./PDFMenu.scss"; import { observable, action, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { emptyFunction, returnFalse } from "../../../Utils"; +import { unimplementedFunction, returnFalse } from "../../../Utils"; +import AntimodeMenu from "../AntimodeMenu"; import { Doc, Opt } from "../../../new_fields/Doc"; @observer -export default class PDFMenu extends React.Component { +export default class PDFMenu extends AntimodeMenu { static Instance: PDFMenu; - private _offsetY: number = 0; - private _offsetX: number = 0; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _commentCont = React.createRef<HTMLButtonElement>(); private _snippetButton: React.RefObject<HTMLButtonElement> = React.createRef(); - private _dragging: boolean = false; - @observable private _top: number = -300; - @observable private _left: number = -300; - @observable private _opacity: number = 1; - @observable private _transition: string = "opacity 0.5s"; - @observable private _transitionDelay: string = ""; @observable private _keyValue: string = ""; @observable private _valueValue: string = ""; @observable private _added: boolean = false; @observable public Highlighting: boolean = false; @observable public Status: "pdf" | "annotation" | "snippet" | "" = ""; - @observable public Pinned: boolean = false; - public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; + public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined; - public Delete: () => void = emptyFunction; - public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; + public Delete: () => void = unimplementedFunction; + public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = unimplementedFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; - public PinToPres: () => void = emptyFunction; + public PinToPres: () => void = unimplementedFunction; public Marquee: { left: number; top: number; width: number; height: number; } | undefined; constructor(props: Readonly<{}>) { @@ -73,86 +64,11 @@ export default class PDFMenu extends React.Component { } @action - jumpTo = (x: number, y: number, forceJump: boolean = false) => { - if (!this.Pinned || forceJump) { - this._transition = this._transitionDelay = ""; - this._opacity = 1; - this._left = x; - this._top = y; - } - } - - @action - fadeOut = (forceOut: boolean) => { - if (!this.Pinned) { - if (this._opacity === 0.2) { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 0; - this._left = this._top = -300; - } - - if (forceOut) { - this._transition = ""; - this._transitionDelay = ""; - this._opacity = 0; - this._left = this._top = -300; - } - } - } - - @action - pointerLeave = (e: React.PointerEvent) => { - if (!this.Pinned) { - this._transition = "opacity 0.5s"; - this._transitionDelay = "1s"; - this._opacity = 0.2; - setTimeout(() => this.fadeOut(false), 3000); - } - } - - @action - pointerEntered = (e: React.PointerEvent) => { - this._transition = "opacity 0.1s"; - this._transitionDelay = ""; - this._opacity = 1; - } - - @action togglePin = (e: React.MouseEvent) => { this.Pinned = !this.Pinned; !this.Pinned && (this.Highlighting = false); } - dragStart = (e: React.PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.addEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - document.addEventListener("pointerup", this.dragEnd); - - this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX; - this._offsetY = e.nativeEvent.offsetY; - - e.stopPropagation(); - e.preventDefault(); - } - - @action - dragging = (e: PointerEvent) => { - this._left = e.pageX - this._offsetX; - this._top = e.pageY - this._offsetY; - - e.stopPropagation(); - e.preventDefault(); - } - - dragEnd = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.dragging); - document.removeEventListener("pointerup", this.dragEnd); - e.stopPropagation(); - e.preventDefault(); - } - @action highlightClicked = (e: React.MouseEvent) => { if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight @@ -164,11 +80,6 @@ export default class PDFMenu extends React.Component { this.Delete(); } - handleContextMenu = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - } - snippetStart = (e: React.PointerEvent) => { document.removeEventListener("pointermove", this.snippetDrag); document.addEventListener("pointermove", this.snippetDrag); @@ -219,33 +130,27 @@ export default class PDFMenu extends React.Component { render() { let buttons = this.Status === "pdf" || this.Status === "snippet" ? [ - <button key="1" className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> + <button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>, - <button key="2" className="pdfMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> + <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> <FontAwesomeIcon icon="comment-alt" size="lg" /></button>, - <button key="3" className="pdfMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}> + <button key="3" className="antimodeMenu-button" title="Drag to Snippetize Selection" style={{ display: this.Status === "snippet" ? "" : "none" }} onPointerDown={this.snippetStart} ref={this._snippetButton}> <FontAwesomeIcon icon="cut" size="lg" /></button>, - <button key="4" className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> + <button key="4" className="antimodeMenu-button" title="Pin Menu" onClick={this.togglePin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}> <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} /> </button> ] : [ - <button key="5" className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> + <button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> <FontAwesomeIcon icon="trash-alt" size="lg" /></button>, - <button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> + <button key="6" className="antimodeMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}> <FontAwesomeIcon icon="map-pin" size="lg" /></button>, <div key="7" className="pdfMenu-addTag" > <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} /> <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} /> </div>, - <button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> + <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}> <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>, ]; - return ( - <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu} - style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}> - {buttons} - <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> - </div > - ); + return this.getElement(buttons); } }
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index f6fedf3da..ac018aa0e 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -1,11 +1,12 @@ .pdfViewer-viewer, .pdfViewer-viewer-zoomed { - pointer-events: inherit; + pointer-events: all; width: 100%; height: 100%; position: absolute; overflow-y: auto; overflow-x: hidden; + transform-origin: top left; // .canvasWrapper { // transform: scale(0.75); @@ -13,8 +14,7 @@ // } .textLayer { - mix-blend-mode: multiply; - opacity: 0.9; + mix-blend-mode: multiply;// bcz: makes text fuzzy! span { padding-right: 5px; padding-bottom: 4px; @@ -35,26 +35,46 @@ pointer-events: none; } + .pdfViewer-text-selected { + .textLayer{ + pointer-events: all; + user-select: text; + } + } + .pdfViewer-text { + transform-origin: top left; + .textLayer { + will-change: transform; + } + } + .pdfViewer-dragAnnotationBox { position:absolute; background-color: transparent; opacity: 0.1; } - .pdfViewer-overlay { + .pdfViewer-overlay, .pdfViewer-overlay-inking { transform-origin: left top; position: absolute; top: 0px; left: 0px; display: inline-block; width:100%; + pointer-events: none; + } + .pdfViewer-overlay-inking { + .collectionfreeformview-container { + pointer-events: all; + } } .pdfViewer-annotationLayer { position: absolute; + transform-origin: left top; top: 0; width: 100%; pointer-events: none; - mix-blend-mode: multiply; + mix-blend-mode: multiply; // bcz: makes text fuzzy! .pdfViewer-annotationBox { position: absolute; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 0cb671156..f1c500391 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -26,6 +26,10 @@ import { undoBatch } from "../../util/UndoManager"; import { DocAnnotatableComponent } from "../DocComponent"; import { DocumentType } from "../../documents/DocumentTypes"; import { documentSchema } from "../../../new_fields/documentSchemas"; +import { DocumentDecorations } from "../DocumentDecorations"; +import { InkingControl } from "../InkingControl"; +import { InkTool } from "../../../new_fields/InkField"; +import { TraceMobx } from "../../../new_fields/util"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); @@ -56,9 +60,10 @@ interface IViewerProps { startupLive: boolean; renderDepth: number; focus: (doc: Doc) => void; - isSelected: () => boolean; + isSelected: (outsideReaction?: boolean) => boolean; loaded: (nw: number, nh: number, np: number) => void; - active: () => boolean; + active: (outsideReaction?: boolean) => boolean; + isChildActive: (outsideReaction?: boolean) => boolean; addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; addDocument?: (doc: Doc) => boolean; @@ -117,6 +122,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument _lastSearch: string = ""; componentDidMount = async () => { + !this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true); // change the address to be the file address of the PNG version of each page // file address of the pdf this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`))); @@ -161,7 +167,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument } copy = (e: ClipboardEvent) => { - if (this.props.active() && e.clipboardData) { + if (this.props.active(true) && e.clipboardData) { let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish) if (annoDoc) { e.clipboardData.setData("text/plain", this._selectionText); @@ -391,16 +397,20 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument @action onPointerDown = (e: React.PointerEvent): void => { + let hit = document.elementFromPoint(e.clientX, e.clientY); + if (hit && hit.localName === "span" && this.props.isSelected(true)) { // drag selecting text stops propagation + e.button === 0 && e.stopPropagation(); + } // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); if ((this.Document.scale || 1) !== 1) return; - if ((e.button !== 0 || e.altKey) && this.active()) { + if ((e.button !== 0 || e.altKey) && this.active(true)) { this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true); } this._marqueeing = false; - if (!e.altKey && e.button === 0 && this.active()) { + if (!e.altKey && e.button === 0 && this.active(true)) { // clear out old marquees and initialize menu for new selection PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; @@ -461,10 +471,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument let annoBox = document.createElement("div"); annoBox.className = "pdfViewer-annotationBox"; // transforms the positions from screen onto the pdf div - annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString(); - annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString(); - annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width).toString(); - annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height).toString(); + annoBox.style.top = ((rect.top - boundingRect.top) * scaleY / this._zoomed + this._mainCont.current.scrollTop).toString(); + annoBox.style.left = ((rect.left - boundingRect.left) * scaleX / this._zoomed).toString(); + annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width / this._zoomed).toString(); + annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height / this._zoomed).toString(); this.createAnnotation(annoBox, this.getPageFromScroll(rect.top)); } } @@ -605,45 +615,50 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument e.stopPropagation(); if (e.ctrlKey) { let curScale = Number(this._pdfViewer.currentScaleValue); - this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - curScale * e.deltaY / 1000)); this._zoomed = Number(this._pdfViewer.currentScaleValue); } } @computed get annotationLayer() { - trace(); - return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0) }} ref={this._annotationLayer}> + TraceMobx(); + return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}> {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)} - <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}> - <CollectionFreeFormView {...this.props} - annotationsKey={this.annotationsKey} - setPreviewCursor={this.setPreviewCursor} - PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)} - PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)} - VisibleHeight={this.visibleHeight} - focus={this.props.focus} - isSelected={this.props.isSelected} - isAnnotationOverlay={true} - select={emptyFunction} - active={this.active} - ContentScaling={returnOne} - whenActiveChanged={this.whenActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument} - CollectionView={undefined} - ScreenToLocalTransform={this.scrollXf} - ruleProvider={undefined} - renderDepth={this.props.renderDepth + 1} - ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document} - chromeCollapsed={true}> - </CollectionFreeFormView> - </div> + </div>; + } + overlayTransform = () => this.scrollXf().scale(1 / this._zoomed); + panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0); + panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0); + @computed get overlayLayer() { + return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}> + <CollectionFreeFormView {...this.props} + annotationsKey={this.annotationsKey} + setPreviewCursor={this.setPreviewCursor} + PanelHeight={this.panelWidth} + PanelWidth={this.panelHeight} + VisibleHeight={this.visibleHeight} + focus={this.props.focus} + isSelected={this.props.isSelected} + isAnnotationOverlay={true} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={this.contentZoom} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.overlayTransform} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document} + chromeCollapsed={true}> + </CollectionFreeFormView> </div>; } @computed get pdfViewerDiv() { - return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />; + return <div className={"pdfViewer-text" + ((!DocumentDecorations.Instance.Interacting && (this.props.isSelected() || this.props.isChildActive())) ? "-selected" : "")} ref={this._viewer} />; } @computed get standinViews() { return <> @@ -657,11 +672,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument marqueeY = () => this._marqueeY; marqueeing = () => this._marqueeing; visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; + contentZoom = () => this._zoomed; + @computed get contentScaling() { return this.props.ContentScaling(); } render() { - trace(); + TraceMobx(); return !this.extensionDoc ? (null) : - <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}> + <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont} + style={{ + width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`, + height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`, + transform: `scale(${this.contentScaling})` + }} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}> {this.pdfViewerDiv} + {this.overlayLayer} {this.annotationLayer} {this.standinViews} <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 08d808e78..77131ca44 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -15,6 +15,7 @@ import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstruct import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util"; import { intersectRect } from "../Utils"; import { UndoManager } from "../client/util/UndoManager"; +import { computedFn } from "mobx-utils"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -638,13 +639,12 @@ export namespace Doc { } export class DocBrush { - @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); + BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); } const brushManager = new DocBrush(); export class DocData { @observable _user_doc: Doc = undefined!; - @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap(); } // the document containing the view layout information - will be the Document itself unless the Document has @@ -655,10 +655,18 @@ export namespace Doc { export function UserDoc(): Doc { return manager._user_doc; } export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } export function IsBrushed(doc: Doc) { - return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)); + return computedFn(function IsBrushed(doc: Doc) { + return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)); + })(doc); + } + // don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message) + export function IsBrushedDegreeUnmemoized(doc: Doc) { + return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0; } export function IsBrushedDegree(doc: Doc) { - return brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 2 : brushManager.BrushedDoc.has(doc) ? 1 : 0; + return computedFn(function IsBrushDegree(doc: Doc) { + return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0; + })(doc); } export function BrushDoc(doc: Doc) { brushManager.BrushedDoc.set(doc, true); diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts index d94834e91..2d8bb582a 100644 --- a/src/new_fields/InkField.ts +++ b/src/new_fields/InkField.ts @@ -12,16 +12,12 @@ export enum InkTool { Scrubber } -export interface StrokeData { - pathData: Array<{ x: number, y: number }>; - color: string; - width: string; - tool: InkTool; - displayTimecode: number; - creationTime: number; +export interface PointData { + x: number; + y: number; } -export type InkData = Map<string, StrokeData>; +export type InkData = Array<PointData>; const pointSchema = createSimpleSchema({ x: true, y: true @@ -34,16 +30,16 @@ const strokeDataSchema = createSimpleSchema({ @Deserializable("ink") export class InkField extends ObjectField { - @serializable(map(object(strokeDataSchema))) + @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; - constructor(data?: InkData) { + constructor(data: InkData) { super(); - this.inkData = data || new Map; + this.inkData = data; } [Copy]() { - return new InkField(DeepCopy(this.inkData)); + return new InkField(this.inkData); } [ToScriptString]() { diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts index fa47374f1..4c2f061a6 100644 --- a/src/new_fields/documentSchemas.ts +++ b/src/new_fields/documentSchemas.ts @@ -41,9 +41,11 @@ export const documentSchema = createSchema({ showTitle: "string", // whether an editable title banner is displayed at tht top of the document isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) - isAnimating: "boolean", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). + isAnimating: "string", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). value is undefined|"min"|"max" animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc. + strokeWidth: "number", + fontSize: "string" }); export const positionSchema = createSchema({ diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 92e3e3f6f..f748c7638 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper"; import { ProxyField } from "./Proxy"; import { RefField } from "./RefField"; import { ObjectField } from "./ObjectField"; -import { action } from "mobx"; +import { action, trace } from "mobx"; import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols"; import { DocServer } from "../client/DocServer"; @@ -12,6 +12,10 @@ function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } +export function TraceMobx() { + //trace(); +} + export interface GetterResult { value: FieldResult; shouldReturn?: boolean; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 466e2d707..5b9bba47d 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -46,7 +46,7 @@ export class CurrentUserUtils { let notes = CurrentUserUtils.setupNoteTypes(doc); doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 }); doc.activePen = doc; - let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ + let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [ { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' }, { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] }, { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' }, @@ -55,16 +55,16 @@ export class CurrentUserUtils { { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' }, { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' }, { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' }, - { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc }, - { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, - { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc }, - { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc }, + { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc }, + { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc }, + { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc }, + { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc }, ]; return docProtoData.map(data => Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen, + ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory, })); } @@ -82,7 +82,7 @@ export class CurrentUserUtils { }); return Docs.Create.ButtonDocument({ - width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Tools", targetContainer: sidebarContainer, + width: 35, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Tools", fontSize: 10, targetContainer: sidebarContainer, sourcePanel: Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, lockedPosition: true, chromeStatus: "disabled", title: "tools stack" }), @@ -107,19 +107,19 @@ export class CurrentUserUtils { }); return Docs.Create.ButtonDocument({ - width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library", + width: 50, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Library", fontSize: 10, sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], { title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true }), targetContainer: sidebarContainer, - onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel") + onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") }); } // setup the Search button which will display the search panel. static setupSearchPanel(sidebarContainer: Doc) { return Docs.Create.ButtonDocument({ - width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search", + width: 50, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Search", fontSize: 10, sourcePanel: Docs.Create.QueryDocument({ title: "search stack", ignoreClick: true }), |