diff options
author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-04-30 16:21:53 -0400 |
---|---|---|
committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-04-30 16:21:53 -0400 |
commit | 43ed4e7fd2d6120598733e537a301a8f87379239 (patch) | |
tree | 2dc0b21a0da4e25b899ab46a8ca512131cb23ccb | |
parent | d99b1be8e44ed2dc65714ea192debf5529e353de (diff) | |
parent | c95e1789da41fb63e27f1086e30c0ebd151009df (diff) |
Merge branch 'newDocs' of https://github.com/browngraphicslab/Dash-Web into newDocs
52 files changed, 1105 insertions, 660 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2a9687bda..964faa8db 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -18,6 +18,8 @@ import { action } from "mobx"; import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; import { AggregateFunction } from "../northstar/model/idea/idea"; +import { Template } from "../views/Templates"; +import { TemplateField } from "../../fields/TemplateField"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { Field, Doc, Opt } from "../../new_fields/Doc"; @@ -44,7 +46,9 @@ export interface DocumentOptions { panY?: number; page?: number; scale?: number; + baseLayout?: string; layout?: string; + //templates?: Array<Template>; viewType?: number; backgroundColor?: string; copyDraggedItems?: boolean; @@ -52,6 +56,7 @@ export interface DocumentOptions { curPage?: number; documentText?: string; borderRounding?: number; + schemaColumns?: List<string>; // [key: string]: Opt<Field>; } const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; @@ -94,7 +99,7 @@ export namespace Docs { } function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc { - return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout }); + return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout, baseLayout: layout }); } function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) { const deleg = Doc.MakeDelegate(doc); @@ -120,6 +125,7 @@ export namespace Docs { function CreateIconPrototype(): Doc { let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) }); + console.log("iconpr" + iconProto.layout) return iconProto; } function CreateTextPrototype(): Doc { @@ -234,13 +240,13 @@ export namespace Docs { if (!makePrototype) { return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents)); } - return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Freeform }); + return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["schemaColumns"]), ...options, viewType: CollectionViewType.Freeform }); } export function SchemaDocument(documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Schema }); + return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["schemaColumns"]), ...options, viewType: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Tree }); + return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["schemaColumns"]), ...options, viewType: CollectionViewType.Tree }); } export function DockDocument(config: string, options: DocumentOptions) { return CreateInstance(collProto, config, { ...options, viewType: CollectionViewType.Docking }); diff --git a/src/client/northstar/core/filter/ValueComparision.ts b/src/client/northstar/core/filter/ValueComparision.ts index 80b1242a9..65687a82b 100644 --- a/src/client/northstar/core/filter/ValueComparision.ts +++ b/src/client/northstar/core/filter/ValueComparision.ts @@ -62,13 +62,13 @@ export class ValueComparison { var rawName = this.attributeModel.CodeName; switch (this.Predicate) { case Predicate.STARTS_WITH: - ret += rawName + " !== null && " + rawName + ".StartsWith(" + val + ") "; + ret += rawName + " != null && " + rawName + ".StartsWith(" + val + ") "; return ret; case Predicate.ENDS_WITH: - ret += rawName + " !== null && " + rawName + ".EndsWith(" + val + ") "; + ret += rawName + " != null && " + rawName + ".EndsWith(" + val + ") "; return ret; case Predicate.CONTAINS: - ret += rawName + " !== null && " + rawName + ".Contains(" + val + ") "; + ret += rawName + " != null && " + rawName + ".Contains(" + val + ") "; return ret; default: ret += rawName + " " + op + " " + val + " "; diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index 19d108676..765ecf8f0 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -1,7 +1,6 @@ import React = require("react"); import { action, computed, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; -import Measure from "react-measure"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange'; import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper"; @@ -32,8 +31,6 @@ export class HistogramBox extends React.Component<FieldViewProps> { private _dropXDisposer?: DragManager.DragDropDisposer; private _dropYDisposer?: DragManager.DragDropDisposer; - @observable public PanelWidth: number = 100; - @observable public PanelHeight: number = 100; @observable public HistoOp: HistogramOperation = HistogramOperation.Empty; @observable public VisualBinRanges: VisualBinRange[] = []; @observable public ValueRange: number[] = []; @@ -89,7 +86,7 @@ export class HistogramBox extends React.Component<FieldViewProps> { } reaction(() => CurrentUserUtils.NorthstarDBCatalog, (catalog?: Catalog) => this.activateHistogramOperation(catalog), { fireImmediately: true }); reaction(() => [this.VisualBinRanges && this.VisualBinRanges.slice()], () => this.SizeConverter.SetVisualBinRanges(this.VisualBinRanges)); - reaction(() => [this.PanelHeight, this.PanelWidth], () => this.SizeConverter.SetIsSmall(this.PanelWidth < 40 && this.PanelHeight < 40)); + reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], (size: number[]) => this.SizeConverter.SetIsSmall(size[0] < 40 && size[1] < 40)); reaction(() => this.HistogramResult ? this.HistogramResult.binRanges : undefined, (binRanges: BinRange[] | undefined) => { if (binRanges) { @@ -138,38 +135,39 @@ export class HistogramBox extends React.Component<FieldViewProps> { }); } } + + @action + private onScrollWheel = (e: React.WheelEvent) => { + this.HistoOp.DrillDown(e.deltaY > 0); + e.stopPropagation(); + } + render() { let labelY = this.HistoOp && this.HistoOp.Y ? this.HistoOp.Y.PresentedName : "<...>"; let labelX = this.HistoOp && this.HistoOp.X ? this.HistoOp.X.PresentedName : "<...>"; - var h = this.props.isTopMost ? this.PanelHeight : NumCast(this.props.Document.height); - var w = this.props.isTopMost ? this.PanelWidth : NumCast(this.props.Document.width); let loff = this.SizeConverter.LeftOffset; let toff = this.SizeConverter.TopOffset; let roff = this.SizeConverter.RightOffset; let boff = this.SizeConverter.BottomOffset; return ( - <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height; })}> - {({ measureRef }) => - <div className="histogrambox-container" ref={measureRef}> - <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} > - <span className="histogrambox-yaxislabel-text"> - {labelY} - </span> - </div> - <div className="histogrambox-primitives" style={{ - transform: `translate(${loff + 25}px, ${toff}px)`, - width: `calc(100% - ${loff + roff + 25}px)`, - height: `calc(100% - ${toff + boff}px)`, - }}> - <HistogramLabelPrimitives HistoBox={this} /> - <HistogramBoxPrimitives HistoBox={this} /> - </div> - <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} > - {labelX} - </div> - </div> - } - </Measure> + <div className="histogrambox-container" onWheel={this.onScrollWheel}> + <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} > + <span className="histogrambox-yaxislabel-text"> + {labelY} + </span> + </div> + <div className="histogrambox-primitives" style={{ + transform: `translate(${loff + 25}px, ${toff}px)`, + width: `calc(100% - ${loff + roff + 25}px)`, + height: `calc(100% - ${toff + boff}px)`, + }}> + <HistogramLabelPrimitives HistoBox={this} /> + <HistogramBoxPrimitives HistoBox={this} /> + </div> + <div className="histogrambox-xaxislabel" onPointerDown={this.xLabelPointerDown} ref={this._dropXRef} > + {labelX} + </div> + </div> ); } } diff --git a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx index 5785fe838..62aebd3c6 100644 --- a/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx +++ b/src/client/northstar/dash-nodes/HistogramLabelPrimitives.tsx @@ -12,7 +12,7 @@ import { HistogramPrimitivesProps } from "./HistogramBoxPrimitives"; @observer export class HistogramLabelPrimitives extends React.Component<HistogramPrimitivesProps> { componentDidMount() { - reaction(() => [this.props.HistoBox.PanelWidth, this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], + reaction(() => [this.props.HistoBox.props.PanelWidth(), this.props.HistoBox.SizeConverter.LeftOffset, this.props.HistoBox.VisualBinRanges.length], (fields) => HistogramLabelPrimitives.computeLabelAngle(fields[0], fields[1], this.props.HistoBox), { fireImmediately: true }); } @@ -35,7 +35,7 @@ export class HistogramLabelPrimitives extends React.Component<HistogramPrimitive if (!vb.length || !sc.Initialized) { return (null); } - let dim = (axis === 0 ? this.props.HistoBox.PanelWidth : this.props.HistoBox.PanelHeight) / ((axis === 0 && vb[axis] instanceof NominalVisualBinRange) ? + let dim = (axis === 0 ? this.props.HistoBox.props.PanelWidth() : this.props.HistoBox.props.PanelHeight()) / ((axis === 0 && vb[axis] instanceof NominalVisualBinRange) ? (12 + 5) : // (<number>FontStyles.AxisLabel.fontSize + 5))); sc.MaxLabelSizes[axis].coords[axis] + 5); @@ -49,12 +49,12 @@ export class HistogramLabelPrimitives extends React.Component<HistogramPrimitive let yStart = (axis === 1 ? r.yFrom - textHeight / 2 : r.yFrom); if (axis === 0 && vb[axis] instanceof NominalVisualBinRange) { - let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.PanelWidth; + let space = (r.xTo - r.xFrom) / sc.RenderDimension * this.props.HistoBox.props.PanelWidth(); xStart += Math.max(textWidth / 2, (1 - textWidth / space) * textWidth / 2) - textHeight / 2; } let xPercent = axis === 1 ? `${xStart}px` : `${xStart / sc.RenderDimension * 100}%`; - let yPercent = axis === 0 ? `${this.props.HistoBox.PanelHeight - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%`; + let yPercent = axis === 0 ? `${this.props.HistoBox.props.PanelHeight() - sc.BottomOffset - textHeight}px` : `${yStart / sc.RenderDimension * 100}%`; prims.push( <div className="histogramLabelPrimitives-placer" key={DashUtils.GenerateGuid()} style={{ transform: `translate(${xPercent}, ${yPercent})` }}> diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts index b6672eca3..5c9c832c0 100644 --- a/src/client/northstar/operations/HistogramOperation.ts +++ b/src/client/northstar/operations/HistogramOperation.ts @@ -22,7 +22,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons @observable public Links: Doc[] = []; @observable public BrushLinks: { l: Doc, b: Doc }[] = []; @observable public BrushColors: number[] = []; - @observable public FilterModels: FilterModel[] = []; + @observable public BarFilterModels: FilterModel[] = []; @observable public Normalization: number = -1; @observable public X: AttributeTransformationModel; @@ -49,17 +49,24 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons throw new Error("Method not implemented."); } + + @computed public get FilterModels() { + return this.BarFilterModels; + } @action public AddFilterModels(filterModels: FilterModel[]): void { - filterModels.filter(f => f !== null).forEach(fm => this.FilterModels.push(fm)); + filterModels.filter(f => f !== null).forEach(fm => this.BarFilterModels.push(fm)); } @action public RemoveFilterModels(filterModels: FilterModel[]): void { - ArrayUtil.RemoveMany(this.FilterModels, filterModels); + ArrayUtil.RemoveMany(this.BarFilterModels, filterModels); } @computed public get FilterString(): string { + if (this.OverridingFilters.length > 0) { + return "(" + this.OverridingFilters.filter(fm => fm != null).map(fm => fm.ToPythonString()).join(" || ") + ")"; + } let filterModels: FilterModel[] = []; return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true); } @@ -78,6 +85,27 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons return brushes; } + _stackedFilters: (FilterModel[])[] = []; + @action + public DrillDown(up: boolean) { + if (!up) { + if (!this.BarFilterModels.length) + return; + this._stackedFilters.push(this.BarFilterModels.map(f => f)); + this.OverridingFilters.length = 0; + this.OverridingFilters.push(...this._stackedFilters[this._stackedFilters.length - 1]); + this.BarFilterModels.map(fm => fm).map(fm => this.RemoveFilterModels([fm])); + //this.updateHistogram(); + } else { + this.OverridingFilters.length = 0; + if (this._stackedFilters.length) { + this.OverridingFilters.push(...this._stackedFilters.pop()!); + } + // else + // this.updateHistogram(); + } + } + private getAggregateParameters(histoX: AttributeTransformationModel, histoY: AttributeTransformationModel, histoValue: AttributeTransformationModel) { let allAttributes = new Array<AttributeTransformationModel>(histoX, histoY, histoValue); allAttributes = ArrayUtil.Distinct(allAttributes.filter(a => a.AggregateFunction !== AggregateFunction.None)); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index e65f2b9ed..a149da52f 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -172,6 +172,8 @@ export namespace DragManager { StartDrag([ele], dragData, downX, downY, options); } + export let AbortDrag: () => void = emptyFunction; + function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) { if (!dragDiv) { dragDiv = document.createElement("div"); @@ -272,23 +274,32 @@ export namespace DragManager { ); }; - AbortDrag = () => { + let hideDragElements = () => { + dragElements.map(dragElement => dragElement.parentNode == dragDiv && dragDiv.removeChild(dragElement)); + eles.map(ele => (ele.hidden = false)); + }; + let endDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - dragElements.map(dragElement => { if (dragElement.parentNode === dragDiv) dragDiv.removeChild(dragElement); }); - eles.map(ele => (ele.hidden = false)); + if (options) { + options.handlers.dragComplete({}); + } + } + + AbortDrag = () => { + hideDragElements(); + endDrag(); }; const upHandler = (e: PointerEvent) => { - AbortDrag(); - FinishDrag(eles, e, dragData, options, finishDrag); + hideDragElements(); + dispatchDrag(eles, e, dragData, options, finishDrag); + endDrag(); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - export let AbortDrag: () => void = emptyFunction; - - function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { + function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) { let removed = dragEles.map(dragEle => { let parent = dragEle.parentElement; if (parent) parent.removeChild(dragEle); @@ -313,11 +324,6 @@ export namespace DragManager { } }) ); - - if (options) { - options.handlers.dragComplete({}); - } } - DocumentDecorations.Instance.Hidden = false; } } diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 1b6647003..68a73375e 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -11,7 +11,8 @@ import React = require("react"); import "./TooltipTextMenu.scss"; const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); import { library } from '@fortawesome/fontawesome-svg-core'; -import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { wrapInList, bulletList, liftListItem, listItem, } from 'prosemirror-schema-list'; +import { liftTarget } from 'prosemirror-transform'; import { faListUl, } from '@fortawesome/free-solid-svg-icons'; @@ -24,19 +25,22 @@ const SVG = "http://www.w3.org/2000/svg"; //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. export class TooltipTextMenu { - private tooltip: HTMLElement; + public tooltip: HTMLElement; private num_icons = 0; private view: EditorView; private fontStyles: MarkType[]; private fontSizes: MarkType[]; + private listTypes: NodeType[]; private editorProps: FieldViewProps; private state: EditorState; private fontSizeToNum: Map<MarkType, number>; private fontStylesToName: Map<MarkType, string>; + private listTypeToIcon: Map<NodeType, string>; private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); //dropdown doms private fontSizeDom?: Node; private fontStyleDom?: Node; + private listTypeBtnDom?: Node; constructor(view: EditorView, editorProps: FieldViewProps) { this.view = view; @@ -58,8 +62,9 @@ export class TooltipTextMenu { { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, - { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, - { command: lift, dom: this.icon("<", "lift") }, + // { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, + // { command: wrapInList(schema.nodes.ordered_list), dom: this.icon("1)", "bullets") }, + // { command: lift, dom: this.icon("<", "lift") }, ]; //add menu items items.forEach(({ dom, command }) => { @@ -96,7 +101,11 @@ export class TooltipTextMenu { this.fontSizeToNum.set(schema.marks.p72, 72); this.fontSizes = Array.from(this.fontSizeToNum.keys()); - //this.addFontDropdowns(); + //list types + this.listTypeToIcon = new Map(); + this.listTypeToIcon.set(schema.nodes.bullet_list, ":"); + this.listTypeToIcon.set(schema.nodes.ordered_list, "1)"); + this.listTypes = Array.from(this.listTypeToIcon.keys()); this.update(view, undefined); } @@ -109,7 +118,7 @@ export class TooltipTextMenu { //font SIZES let fontSizeBtns: MenuItem[] = []; this.fontSizeToNum.forEach((number, mark) => { - fontSizeBtns.push(this.dropdownBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); + fontSizeBtns.push(this.dropdownMarkBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); }); if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } @@ -128,7 +137,7 @@ export class TooltipTextMenu { //font STYLES let fontBtns: MenuItem[] = []; this.fontStylesToName.forEach((name, mark) => { - fontBtns.push(this.dropdownBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); + fontBtns.push(this.dropdownMarkBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); }); if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } @@ -140,6 +149,29 @@ export class TooltipTextMenu { this.tooltip.appendChild(this.fontStyleDom); } + //will display a remove-list-type button if selection is in list, otherwise will show list type dropdown + updateListItemDropdown(label: string, listTypeBtn: Node) { + //remove old btn + if (listTypeBtn) { this.tooltip.removeChild(listTypeBtn); } + + //Make a dropdown of all list types + let toAdd: MenuItem[] = []; + this.listTypeToIcon.forEach((icon, type) => { + toAdd.push(this.dropdownNodeBtn(icon, "width: 40px;", type, this.view, this.listTypes, this.changeToNodeType)); + }); + //option to remove the list formatting + toAdd.push(this.dropdownNodeBtn("X", "width: 40px;", undefined, this.view, this.listTypes, this.changeToNodeType)); + + listTypeBtn = (new Dropdown(toAdd, { + label: label, + css: "color:white; width: 40px;" + }) as MenuItem).render(this.view).dom; + + //add this new button and return it + this.tooltip.appendChild(listTypeBtn); + return listTypeBtn; + } + //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { let { empty, $cursor, ranges } = view.state.selection as TextSelection; @@ -171,9 +203,18 @@ export class TooltipTextMenu { return toggleMark(markType)(view.state, view.dispatch, view); } - //makes a button for the drop down + //remove all node typeand apply the passed-in one to the selected text + changeToNodeType(nodeType: NodeType | undefined, view: EditorView, allNodes: NodeType[]) { + //remove old + liftListItem(schema.nodes.list_item)(view.state, view.dispatch); + if (nodeType) { //add new + wrapInList(nodeType)(view.state, view.dispatch); + } + } + + //makes a button for the drop down FOR MARKS //css is the style you want applied to the button - dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) { + dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) { return new MenuItem({ title: "", label: label, @@ -186,6 +227,23 @@ export class TooltipTextMenu { } }); } + + //makes a button for the drop down FOR NODE TYPES + //css is the style you want applied to the button + dropdownNodeBtn(label: string, css: string, nodeType: NodeType | undefined, view: EditorView, groupNodes: NodeType[], changeToNodeInGroup: (nodeType: NodeType<any> | undefined, view: EditorView, groupNodes: NodeType[]) => any) { + return new MenuItem({ + title: "", + label: label, + execEvent: "", + class: "menuicon", + css: css, + enable(state) { return true; }, + run() { + changeToNodeInGroup(nodeType, view, groupNodes); + } + }); + } + // Helper function to create menu icons icon(text: string, name: string) { let span = document.createElement("span"); @@ -262,6 +320,9 @@ export class TooltipTextMenu { this.tooltip.style.width = 225 + "px"; this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + //UPDATE LIST ITEM DROPDOWN + this.listTypeBtnDom = this.updateListItemDropdown(":", this.listTypeBtnDom!); + //UPDATE FONT STYLE DROPDOWN let activeStyles = this.activeMarksOnSelection(this.fontStyles); if (activeStyles.length === 1) { @@ -288,7 +349,7 @@ export class TooltipTextMenu { } } - //finds all active marks on selection + //finds all active marks on selection in given group activeMarksOnSelection(markGroup: MarkType[]) { //current selection let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index f7c3e5a7b..0b5280c4a 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -141,10 +141,10 @@ export namespace UndoManager { }); //TODO Make this return the return value - export function RunInBatch(fn: () => void, batchName: string) { + export function RunInBatch<T>(fn: () => T, batchName: string) { let batch = StartBatch(batchName); try { - runInAction(fn); + return runInAction(fn); } finally { batch.end(); } diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 0964d5ff3..5008ddfcf 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index f78bf9ff8..0dfa6f0ff 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -3,18 +3,19 @@ .documentDecorations { position: absolute; } + .documentDecorations-container { z-index: $docDecorations-zindex; position: absolute; top: 0; - left:0; + left: 0; display: grid; grid-template-rows: 20px 8px 1fr 8px; grid-template-columns: 8px 16px 1fr 8px 8px; pointer-events: none; #documentDecorations-centerCont { - grid-column:3; + grid-column: 3; background: none; } @@ -39,8 +40,8 @@ #documentDecorations-bottomRightResizer, #documentDecorations-topRightResizer, #documentDecorations-rightResizer { - grid-column-start:5; - grid-column-end:7; + grid-column-start: 5; + grid-column-end: 7; } #documentDecorations-topLeftResizer, @@ -63,16 +64,16 @@ cursor: ew-resize; } .title{ - width:100%; background: lightblue; - grid-column-start:3; + grid-column-start: 3; grid-column-end: 4; pointer-events: auto; + overflow: hidden; } } .documentDecorations-closeButton { - background:$alt-accent; + background: $alt-accent; opacity: 0.8; grid-column-start: 4; grid-column-end: 6; @@ -80,8 +81,9 @@ text-align: center; cursor: pointer; } + .documentDecorations-minimizeButton { - background:$alt-accent; + background: $alt-accent; opacity: 0.8; grid-column-start: 1; grid-column-end: 3; @@ -94,6 +96,7 @@ width: $MINIMIZED_ICON_SIZE; height: $MINIMIZED_ICON_SIZE; } + .documentDecorations-background { background: lightblue; position: absolute; @@ -105,21 +108,9 @@ margin-left: 25px; } -.linkButton-empty:hover { - background: $main-accent; - transform: scale(1.05); - cursor: pointer; -} - -.linkButton-nonempty:hover { - background: $main-accent; - transform: scale(1.05); - cursor: pointer; -} - .linkButton-linker { - position:absolute; - bottom:0px; + position: absolute; + bottom: 0px; left: 0px; height: 20px; width: 20px; @@ -139,13 +130,14 @@ justify-content: center; align-items: center; } + .linkButton-tray { grid-column: 1/4; } -.linkButton-empty { + +.linkButton-empty, .linkButton-nonempty { height: 20px; width: 20px; - margin-top: 10px; border-radius: 50%; opacity: 0.9; pointer-events: auto; @@ -159,23 +151,51 @@ display: flex; justify-content: center; align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } } -.linkButton-nonempty { - height: 20px; - width: 20px; - margin-top: 10px; - border-radius: 50%; - opacity: 0.9; +.templating-menu { + position: absolute; + bottom: 0; + left: 50px; pointer-events: auto; - background-color: $dark-color; - color: $light-color; - 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; + 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: 0; + left: 30px; + width: 150px; + line-height: 25px; + max-height: 175px; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + input { + margin-right: 10px; + } + } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6e361d76a..084220f76 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable, runInAction } from "mobx"; +import { action, computed, observable, runInAction, untracked, reaction } from "mobx"; import { observer } from "mobx-react"; import { emptyFunction, Utils } from "../../Utils"; import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; @@ -8,7 +8,9 @@ import './DocumentDecorations.scss'; import { MainOverlayTextBox } from "./MainOverlayTextBox"; import { DocumentView, PositionDocument } from "./nodes/DocumentView"; import { LinkMenu } from "./nodes/LinkMenu"; +import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); +import { Template, Templates } from "./Templates"; import { CompileScript } from "../util/Scripting"; import { IconBox } from "./nodes/IconBox"; import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types"; @@ -26,9 +28,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _isPointerDown = false; private _resizing = ""; private keyinput: React.RefObject<HTMLInputElement>; - private _documents: DocumentView[] = SelectionManager.SelectedDocuments(); private _resizeBorderWidth = 16; - private _linkBoxHeight = 30; + private _linkBoxHeight = 20; private _titleHeight = 20; private _linkButton = React.createRef<HTMLDivElement>(); private _linkerButton = React.createRef<HTMLDivElement>(); @@ -36,67 +37,76 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _downY = 0; @observable private _minimizedX = 0; @observable private _minimizedY = 0; - //@observable private _title: string = this._documents[0].props.Document.Title; - @observable private _title: string = this._documents.length > 0 ? Cast(this._documents[0].props.Document.title, "string", "") : ""; - @observable private _fieldKey: string = "title"; + @observable private _title: string = ""; + @observable private _edtingTitle = false; + @observable private _fieldKey = "title"; @observable private _hidden = false; @observable private _opacity = 1; - @observable private _dragging = false; @observable private _iconifying = false; @observable public Interacting = false; - constructor(props: Readonly<{}>) { super(props); DocumentDecorations.Instance = this; - this.handleChange = this.handleChange.bind(this); this.keyinput = React.createRef(); + reaction(() => SelectionManager.SelectedDocuments().slice(), (docs) => docs.length === 0 && (this._edtingTitle = false)); } - @action - handleChange = (event: any) => { - this._title = event.target.value; - } - - @action - enterPressed = (e: any) => { + @action titleChanged = (event: any) => { this._title = event.target.value; } + @action titleBlur = () => { this._edtingTitle = false; } + @action titleEntered = (e: any) => { var key = e.keyCode || e.which; // enter pressed if (key === 13) { var text = e.target.value; if (text[0] === '#') { - let command = text.slice(1, text.length); - this._fieldKey = command; - // if (command === "Title" || command === "title") { - // this._fieldKey = KeyStore.Title; - // } - // else if (command === "Width" || command === "width") { - // this._fieldKey = KeyStore.Width; - // } - this._title = "changed"; - // TODO: Change field with switch statement + this._fieldKey = text.slice(1, text.length); + this._title = this.selectionTitle; } else { - if (this._documents.length > 0) { - let field = this._documents[0].props.Document[this._fieldKey]; - if (typeof field === "string") { - this._documents.forEach(d => - d.props.Document[this._fieldKey] = this._title); - } - else if (typeof field === "number") { - this._documents.forEach(d => + if (SelectionManager.SelectedDocuments().length > 0) { + let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey]; + if (typeof field === "number") { + SelectionManager.SelectedDocuments().forEach(d => d.props.Document[this._fieldKey] = +this._title); + } else { + SelectionManager.SelectedDocuments().forEach(d => + d.props.Document[this._fieldKey] = this._title); } - this._title = "changed"; } } e.target.blur(); } } + @action onTitleDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + e.stopPropagation(); + this.onBackgroundDown(e); + document.removeEventListener("pointermove", this.onTitleMove); + document.removeEventListener("pointerup", this.onTitleUp); + document.addEventListener("pointermove", this.onTitleMove); + document.addEventListener("pointerup", this.onTitleUp); + } + @action onTitleMove = (e: PointerEvent): void => { + if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { + this.Interacting = true; + } + if (this.Interacting) this.onBackgroundMove(e); + e.stopPropagation(); + } + @action onTitleUp = (e: PointerEvent): void => { + if (Math.abs(e.clientX - this._downX) < 4 || Math.abs(e.clientY - this._downY) < 4) { + this._title = this.selectionTitle; + this._edtingTitle = true; + } + document.removeEventListener("pointermove", this.onTitleMove); + document.removeEventListener("pointerup", this.onTitleUp); + this.onBackgroundUp(e); + } @computed get Bounds(): { x: number, y: number, b: number, r: number } { - this._documents = SelectionManager.SelectedDocuments(); return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { if (documentView.props.isTopMost) { return bounds; @@ -111,47 +121,39 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); } - - @computed - public get Hidden() { return this._hidden; } - public set Hidden(value: boolean) { this._hidden = value; } - - _lastDrag: number[] = [0, 0]; onBackgroundDown = (e: React.PointerEvent): void => { document.removeEventListener("pointermove", this.onBackgroundMove); - document.addEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); + document.addEventListener("pointermove", this.onBackgroundMove); document.addEventListener("pointerup", this.onBackgroundUp); - this._lastDrag = [e.clientX, e.clientY]; e.stopPropagation(); - if (e.currentTarget.localName !== "input") { - e.preventDefault(); - } + e.preventDefault(); } @action onBackgroundMove = (e: PointerEvent): void => { let dragDocView = SelectionManager.SelectedDocuments()[0]; const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); + let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); dragData.xOffset = xoff; dragData.yOffset = yoff; dragData.aliasOnDrop = false; - let move = SelectionManager.SelectedDocuments()[0].props.moveDocument; - dragData.moveDocument = move; - this.Interacting = this._dragging = true; + dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument; + this.Interacting = true; + this._hidden = true; document.removeEventListener("pointermove", this.onBackgroundMove); 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, { - handlers: { - dragComplete: action(() => this.Interacting = this._dragging = false), - }, + handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) }, hideSource: true }); e.stopPropagation(); } + @action onBackgroundUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); @@ -229,26 +231,30 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> iconDoc.nativeHeight = 0; iconDoc.x = NumCast(doc.x); iconDoc.y = NumCast(doc.y) - 24; - iconDoc.proto = doc; iconDoc.maximizedDoc = doc; doc.minimizedDoc = iconDoc; + console.log("Layout " + iconDoc.layout) docView.props.addDocument && docView.props.addDocument(iconDoc, false); return iconDoc; } @action public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => { let doc = docView.props.Document; - - const minDoc = await Cast(doc.minimizedDoc, Doc); - if (minDoc) return minDoc; - - const field = StrCast(doc.backgroundLayout, undefined); - if (field !== undefined) return this.createIcon(docView, field); - - const layout = StrCast(doc.layout, undefined); - if (layout !== undefined) return this.createIcon(docView, field); - - return undefined; + let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc); + if (!iconDoc) { + const field = StrCast(doc.backgroundLayout, undefined); + if (field !== undefined) { + iconDoc = this.createIcon(docView, field); + } else { + const layout = StrCast(doc.layout, undefined); + if (layout !== undefined) { + iconDoc = this.createIcon(docView, field); + } + } + } + if (SelectionManager.SelectedDocuments()[0].props.addDocument !== undefined) + SelectionManager.SelectedDocuments()[0].props.addDocument!(iconDoc!); + return iconDoc; } @action onMinimizeUp = (e: PointerEvent): void => { @@ -262,7 +268,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> minDocs.map(minDoc => { minDoc.x = NumCast(minDocs[0].x); minDoc.y = NumCast(minDocs[0].y); - minDoc.linkTags = new List(minDocs); + minDoc.linkTags = new List(minDocs.filter(d => d != minDoc)); if (this._iconifying && selectedDocs[0].props.removeDocument) { selectedDocs[0].props.removeDocument(minDoc); (minDoc.maximizedDoc as Doc).minimizedDoc = undefined; @@ -450,17 +456,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } - getValue = (): string => { - if (this._documents.length > 0) { - let field = this._documents[0].props.Document[this._fieldKey]; + @computed + get selectionTitle(): string { + if (SelectionManager.SelectedDocuments().length === 1) { + let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey]; if (typeof field === "string") { return field; } else if (typeof field === "number") { return field.toString(); } + } else if (SelectionManager.SelectedDocuments().length > 1) { + return "-multiple-"; } - return this._title; + return "-unset-"; } changeFlyoutContent = (): void => { @@ -469,10 +478,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> // buttonOnPointerUp = (e: React.PointerEvent): void => { // e.stopPropagation(); // } + render() { var bounds = this.Bounds; let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; - if (bounds.x === Number.MAX_VALUE || !seldoc) { + if (bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } let minimizeIcon = ( @@ -480,14 +490,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."} </div>); - if (this.Hidden) { - return (null); - } - if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { - console.log("DocumentDecorations: Bounds Error"); - return (null); - } - let linkButton = null; if (SelectionManager.SelectedDocuments().length > 0) { let selFirst = SelectionManager.SelectedDocuments()[0]; @@ -498,17 +500,30 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> anchorPoint={anchorPoints.RIGHT_TOP} content={<LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent} />}> - {/* <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> */} - <div className={"linkButton-empty"} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> - </Flyout>); + <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> + </Flyout >); } + + let templates: Map<Template, boolean> = new Map(); + let doc = SelectionManager.SelectedDocuments()[0]; + Array.from(Object.values(Templates.TemplateList)).map(template => { + let docTemps = doc.templates; + let checked = false; + docTemps.forEach(temp => { + if (template.Name === temp.Name) { + checked = true; + } + }); + templates.set(template, checked); + }); + return (<div className="documentDecorations"> <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: this._dragging ? "none" : "all", + pointerEvents: this.Interacting ? "none" : "all", zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0, }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} > </div> @@ -521,7 +536,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }}> {minimizeIcon} - <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onBackgroundDown} onKeyPress={this.enterPressed} /> + {this._edtingTitle ? + <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> : + <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>} <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> @@ -535,6 +552,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div> + <TemplateMenu doc={doc} templates={templates} /> </div > </div> ); diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 1e26893c5..1c0d13545 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -139,21 +139,25 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { let curPage = NumCast(this.props.Document.curPage, -1); let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { if (strokeData.page === -1 || strokeData.page === curPage) { - paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData} + 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} deleteCallback={this.removeLine} />); + color={strokeData.color} + width={strokeData.width} + tool={strokeData.tool} + deleteCallback={this.removeLine} />); } return paths; }, [] as JSX.Element[]); - return [<svg className={`inkingCanvas-paths-markers`} key="Markers" + return [<svg className={`inkingCanvas-paths-ink`} key="Pens" style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} > - {paths.filter(path => path.props.tool === InkTool.Highlighter)} + {paths.filter(path => path.props.tool !== InkTool.Highlighter)} </svg>, - <svg className={`inkingCanvas-paths-ink`} key="Pens" + <svg className={`inkingCanvas-paths-markers`} key="Markers" style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}> - {paths.filter(path => path.props.tool !== InkTool.Highlighter)} + {paths.filter(path => path.props.tool === InkTool.Highlighter)} </svg>]; } diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss new file mode 100644 index 000000000..cdbfdcff3 --- /dev/null +++ b/src/client/views/InkingStroke.scss @@ -0,0 +1,3 @@ +.inkingStroke-marker { + mix-blend-mode: multiply +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 616299146..37b1f5899 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,14 +1,16 @@ import { observer } from "mobx-react"; -import { observable } from "mobx"; +import { observable, trace } from "mobx"; import { InkingControl } from "./InkingControl"; import React = require("react"); import { InkTool } from "../../new_fields/InkField"; +import "./InkingStroke.scss"; interface StrokeProps { offsetX: number; offsetY: number; id: string; + count: number; line: Array<{ x: number, y: number }>; color: string; width: string; @@ -48,10 +50,12 @@ export class InkingStroke extends React.Component<StrokeProps> { 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 data changes + let marker = this.props.tool === InkTool.Highlighter ? "-marker" : ""; let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none"; return ( - <path d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round" + <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} /> ); } diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 2f899ff28..89fdf595a 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -12,6 +12,10 @@ body { left:0; } +div { + user-select: none; +} + #dash-title { position: absolute; right: 46.5%; @@ -55,7 +59,7 @@ p { ::-webkit-scrollbar { -webkit-appearance: none; height: 5px; - width: 5px; + width: 10px; } ::-webkit-scrollbar-thumb { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index adc15a4bc..3f8314d5f 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -216,7 +216,7 @@ export class Main extends React.Component { let audiourl = "http://techslides.com/demos/samples/sample.mp3"; let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; - let addTextNode = action(() => Docs.TextDocument({ width: 200, height: 200, title: "a text note" })); + let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" })); let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); let addSchemaNode = action(() => Docs.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" })); let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index a7ced36bf..de8b3707d 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -20,7 +20,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> public static Instance: MainOverlayTextBox; @observable public TextDoc?: Doc = undefined; public TextScroll: number = 0; - @observable _textRect: any; @observable _textXf: () => Transform = () => Transform.Identity(); private _textFieldKey: string = "data"; private _textColor: string | null = null; @@ -46,7 +45,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> if (div) { this._textColor = div.style.color; div.style.color = "transparent"; - this._textRect = div.getBoundingClientRect(); this.TextScroll = div.scrollTop; } } @@ -87,13 +85,12 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> } render() { - if (this.TextDoc) { - let toScreenXf = this._textXf().inverse(); - let pt = toScreenXf.transformPoint(0, 0); - let s = 1 / this._textXf().Scale; - return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${pt[0]}px, ${pt[1]}px) scale(${s},${s})`, width: "auto", height: "auto" }} > + if (this.TextDoc && this._textTargetDiv) { + let textRect = this._textTargetDiv.getBoundingClientRect(); + let s = this._textXf().Scale; + return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} > <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} - style={{ width: `${NumCast(this.TextDoc.width)}px`, height: `${NumCast(this.TextDoc.height)}px` }}> + style={{ width: `${NumCast(this.TextDoc.width) * s}px`, height: `${NumCast(this.TextDoc.height) * s}px` }}> <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} /> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 744ec0535..4359ba093 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -26,12 +26,11 @@ export class PreviewCursor extends React.Component<{}> { // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore // the keyPress here. //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); - PreviewCursor.Visible = false; - } else if (e.ctrlKey) { - if (e.key == "v") { + if (e.key.startsWith("F") && !e.key.endsWith("F")) { + } else if (e.key != "Escape" && e.key != "Alt" && e.key != "Shift" && e.key != "Meta" && e.key != "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { + if ((!e.ctrlKey && !e.metaKey) || e.key === "v") { PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); + PreviewCursor.Visible = false; } } } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx new file mode 100644 index 000000000..8eb2fc6c6 --- /dev/null +++ b/src/client/views/TemplateMenu.tsx @@ -0,0 +1,75 @@ +import { observable, computed, action } from "mobx"; +import React = require("react"); +import { observer } from "mobx-react"; +import './DocumentDecorations.scss'; +import { Template } from "./Templates"; +import { DocumentView } from "./nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> { + render() { + if (this.props.template) { + return ( + <li> + <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} /> + {this.props.template.Name} + </li> + ); + } else { + return (null); + } + } +} + +export interface TemplateMenuProps { + doc: DocumentView; + templates: Map<Template, boolean>; +} + +@observer +export class TemplateMenu extends React.Component<TemplateMenuProps> { + + @observable private _hidden: boolean = true; + @observable private _templates: Map<Template, boolean> = this.props.templates; + + + @action + toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { + if (event.target.checked) { + this.props.doc.addTemplate(template); + this._templates.set(template, true); + } else { + this.props.doc.removeTemplate(template); + this._templates.set(template, false); + } + } + + @action + componentWillReceiveProps(nextProps: TemplateMenuProps) { + this._templates = nextProps.templates; + } + + @action + toggleTemplateActivity = (): void => { + this._hidden = !this._hidden; + } + + render() { + let templateMenu: Array<JSX.Element> = []; + this._templates.forEach((checked, template) => { + templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />); + }); + + return ( + <div className="templating-menu" > + <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> + <ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}> + {templateMenu} + </ul> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx new file mode 100644 index 000000000..fef69392d --- /dev/null +++ b/src/client/views/Templates.tsx @@ -0,0 +1,66 @@ +import React = require("react"); + +export enum TemplatePosition { + InnerTop, + InnerBottom, + InnerRight, + InnerLeft, + OutterTop, + OutterBottom, + OutterRight, + OutterLeft, +} + +export class Template { + constructor(name: string, position: TemplatePosition, layout: string) { + this._name = name; + this._position = position; + this._layout = layout; + } + + private _name: string; + private _position: TemplatePosition; + private _layout: string; + + get Name(): string { + return this._name; + } + + get Position(): TemplatePosition { + return this._position; + } + + get Layout(): string { + return this._layout; + } +} + +export namespace Templates { + // export const BasicLayout = new Template("Basic layout", "{layout}"); + + export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom, + `<div><div style="margin:auto; height:calc(100%); width:100%;">{layout}</div><div style="height:(100% + 50px); width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={CaptionKey} /></div></div>` + ); + + export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom, + `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute"><FormattedTextBox {...props} fieldKey={CaptionKey}/></div></div>` + ); + + export const SideCaption = new Template("Side caption", TemplatePosition.OutterRight, + `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox {...props} fieldKey={CaptionKey}/></div> </div>` + ); + + export const Title = new Template("Title", TemplatePosition.InnerTop, + `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div><div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; padding:2px 10px">{Title}</div></div>` + ); + + export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption]; + + export function sortTemplates(a: Template, b: Template) { + if (a.Position < b.Position) { return -1; } + if (a.Position > b.Position) { return 1; } + return 0; + } + +} + diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index ed761d3f3..76adfcdcd 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -8,6 +8,7 @@ import { Doc, FieldResult, Opt } from '../../../new_fields/Doc'; import { listSpec } from '../../../new_fields/Schema'; import { List } from '../../../new_fields/List'; import { Id } from '../../../new_fields/RefField'; +import { SelectionManager } from '../../util/SelectionManager'; export enum CollectionViewType { Invalid, @@ -104,8 +105,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) { let zoom = NumCast(this.props.Document.scale, 1); - let screen = this.props.ScreenToLocalTransform().inverse().Scale / (this.props as any).ContentScaling() * zoom; - doc.zoomBasis = screen; + doc.zoomBasis = zoom; } } return true; @@ -145,6 +145,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { return true; } if (this.removeDocument(doc)) { + SelectionManager.DeselectAll(); return addDocument(doc); } return false; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1574562c6..958335425 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -241,6 +241,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this.props.Document.data = json; if (this.undohack && !this.hack) { this.undohack.end(); + this.undohack = undefined; } this.hack = false; } diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss index 0eca3f1cd..f6fb79582 100644 --- a/src/client/views/collections/CollectionPDFView.scss +++ b/src/client/views/collections/CollectionPDFView.scss @@ -1,20 +1,39 @@ .collectionPdfView-buttonTray { - top : 25px; + top : 15px; left : 20px; position: relative; transform-origin: left top; position: absolute; } +.collectionPdfView-thumb { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: darkgray; +} +.collectionPdfView-slider { + width:25px; + height:25px; + transform-origin: left top; + position: absolute; + background: lightgray; +} .collectionPdfView-cont{ width: 100%; height: 100%; position: absolute; top: 0; left:0; - +} +.collectionPdfView-cont-dragging { + span { + user-select: none; + } } .collectionPdfView-backward { color : white; + font-size: 24px; top :0px; left : 0px; position: absolute; @@ -22,8 +41,9 @@ } .collectionPdfView-forward { color : white; + font-size: 24px; top :0px; - left : 35px; + left : 45px; position: absolute; background-color: rgba(50, 50, 50, 0.2); }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 5a1af354a..b3762206a 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,4 +1,4 @@ -import { action } from "mobx"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { ContextMenu } from "../ContextMenu"; import "./CollectionPDFView.scss"; @@ -17,18 +17,44 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionPDFView, fieldKey); } + @observable _inThumb = false; + private set curPage(value: number) { this.props.Document.curPage = value; } private get curPage() { return NumCast(this.props.Document.curPage, -1); } private get numPages() { return NumCast(this.props.Document.numPages); } @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1; @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1; + @action + onThumbDown = (e: React.PointerEvent) => { + document.addEventListener("pointermove", this.onThumbMove, false); + document.addEventListener("pointerup", this.onThumbUp, false); + e.stopPropagation(); + this._inThumb = true; + } + @action + onThumbMove = (e: PointerEvent) => { + let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height; + this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1)); + e.stopPropagation(); + } + @action + onThumbUp = (e: PointerEvent) => { + this._inThumb = false; + document.removeEventListener("pointermove", this.onThumbMove); + document.removeEventListener("pointerup", this.onThumbUp); + } + nativeWidth = () => NumCast(this.props.Document.nativeWidth); + nativeHeight = () => NumCast(this.props.Document.nativeHeight); private get uIButtons() { - let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let ratio = (this.curPage - 1) / this.numPages * 100; return ( - <div className="collectionPdfView-buttonTray" key="tray" style={{ transform: `scale(${scaling}, ${scaling})` }}> + <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}> <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> + <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} > + <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} /> + </div> </div> ); } @@ -51,7 +77,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> { render() { return ( - <CollectionBaseView {...this.props} className="collectionPdfView-cont" onContextMenu={this.onContextMenu}> + <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}> {this.subView} </CollectionBaseView> ); diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 58d20819b..6add6ada8 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, untracked } from "mobx"; +import { action, computed, observable, untracked, runInAction } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'; @@ -58,7 +58,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable _newKeyName: string = ""; @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); } - @computed get columns() { return Cast(this.props.Document.columns, listSpec("string"), []); } + @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } renderCell = (rowProps: CellInfo) => { @@ -156,9 +156,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action toggleKey = (key: string) => { - let list = Cast(this.props.Document.columns, listSpec("string")); + let list = Cast(this.props.Document.schemaColumns, listSpec("string")); if (list === undefined) { - this.props.Document.columns = list = new List<string>([key]); + this.props.Document.schemaColumns = list = new List<string>([key]); } else { const index = list.indexOf(key); if (index === -1) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 3b2f79be1..3e8a8a442 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -3,4 +3,10 @@ stroke-width: 3; transform: translate(10000px,10000px); pointer-events: all; +} +.collectionfreeformlinkview-linkCircle { + stroke: black; + stroke-width: 3; + transform: translate(10000px,10000px); + pointer-events: all; }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index d4987fc18..3b700b053 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -4,20 +4,37 @@ import "./CollectionFreeFormLinkView.scss"; import React = require("react"); import v5 = require("uuid/v5"); import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; +import { InkingControl } from "../../InkingControl"; export interface CollectionFreeFormLinkViewProps { A: Doc; B: Doc; LinkDocs: Doc[]; + addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + removeDocument: (document: Doc) => boolean; } @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { onPointerDown = (e: React.PointerEvent) => { - this.props.LinkDocs.map(l => - console.log("Link:" + StrCast(l.title))); + if (e.button === 0 && !InkingControl.Instance.selectedTool) { + let a = this.props.A; + let b = this.props.B; + let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2); + let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2); + let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2); + let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2); + this.props.LinkDocs.map(l => { + let width = l[WidthSym](); + l.x = (x1 + x2) / 2 - width / 2; + l.y = (y1 + y2) / 2 + 10; + if (!this.props.removeDocument(l)) this.props.addDocument(l, false); + }); + e.stopPropagation(); + e.preventDefault(); + } } render() { let l = this.props.LinkDocs; @@ -28,10 +45,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2); let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2); return ( - <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown} - style={{ strokeWidth: `${l.length * 5}` }} - x1={`${x1}`} y1={`${y1}`} - x2={`${x2}`} y2={`${y2}`} /> + <> + <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + style={{ strokeWidth: `${l.length * 5}` }} + x1={`${x1}`} y1={`${y1}`} + x2={`${x2}`} y2={`${y2}`} /> + <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" + cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} /> + </> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index f693d55e8..4a75fccff 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -84,6 +84,14 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP if (containerDoc) { equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!); } + if (view.props.ContainingCollectionView) { + let collid = view.props.ContainingCollectionView.props.Document[Id]; + Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []). + filter(child => + child[Id] === collid).map(view => + DocumentManager.Instance.getDocumentViews(view).map(view => + equalViews.push(view))); + } return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document); } @@ -107,7 +115,8 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP ); return drawnPairs; }, [] as { a: Doc, b: Doc, l: Doc[] }[]); - return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); + return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} + removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dcded7648..e54553d2b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from "mobx"; +import { action, computed, trace } from "mobx"; import { observer } from "mobx-react"; import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -19,10 +19,9 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import v5 = require("uuid/v5"); import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; -import { Doc } from "../../../../new_fields/Doc"; +import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types"; import { pageSchema } from "../../nodes/ImageBox"; -import { List } from "../../../../new_fields/List"; import { Id } from "../../../../new_fields/RefField"; export const panZoomSchema = createSchema({ @@ -98,7 +97,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (!NumCast(d.height)) { let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); - d.height = nw && nh ? nh / nw * NumCast(d.Width) : 300; + d.height = nw && nh ? nh / nw * NumCast(d.width) : 300; } this.bringToFront(d); }); @@ -110,12 +109,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - cleanupInteractions = () => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - @action onPointerDown = (e: React.PointerEvent): void => { let childSelected = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), [] as Doc[]).filter(doc => doc).reduce((childSelected, doc) => { var dv = DocumentManager.Instance.getDocumentView(doc); @@ -126,7 +119,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) || (!CollectionFreeFormView.RIGHT_BTN_DRAG && ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) { - this.cleanupInteractions(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); this._lastX = e.pageX; @@ -135,7 +129,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } onPointerUp = (e: PointerEvent): void => { - this.cleanupInteractions(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); } @action @@ -243,11 +238,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { focusDocument = (doc: Doc) => { this.setPan( - Cast(doc.x, "number", 0) + Cast(doc.width, "number", 0) / 2, - Cast(doc.y, "number", 0) + Cast(doc.height, "number", 0) / 2); + NumCast(doc.x) + NumCast(doc.width) / 2, + NumCast(doc.y) + NumCast(doc.height) / 2); this.props.focus(this.props.Document); } + getDocumentViewProps(document: Doc): DocumentViewProps { return { Document: document, @@ -258,8 +254,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ScreenToLocalTransform: this.getTransform, isTopMost: false, selectOnLoad: document[Id] === this._selectOnLoaded, - PanelWidth: () => Cast(document.width, "number", 0),//TODO Types These are inline functions - PanelHeight: () => Cast(document.height, "number", 0), + PanelWidth: document[WidthSym], + PanelHeight: document[HeightSym], ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, focus: this.focusDocument, @@ -268,10 +264,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { }; } - @computed + @computed.struct get views() { let curPage = FieldValue(this.Document.curPage, -1); let docviews = (this.children || []).filter(doc => doc).reduce((prev, doc) => { + if (!FieldValue(doc)) return prev; var page = Cast(doc.page, "number", -1); if (page === curPage || page === -1) { let minim = Cast(doc.isMinimized, "boolean"); @@ -292,19 +289,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } - private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.getDocumentViewProps(this.props.Document)} />]; + private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />]; render() { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} style={{ borderRadius: "inherit" }} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > - {/* <svg viewBox="0 0 180 18" style={{ top: "50%", opacity: 0.05, position: "absolute" }}> - <text y="15" > - {this.props.Document.Title} - </text> - </svg> */} - <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} + <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} @@ -316,7 +308,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { </CollectionFreeFormLinksView> {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */} </CollectionFreeFormViewPannableContents> - <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} /> + <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} /> </MarqueeView> </div> ); @@ -337,12 +329,12 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> { } @observer -class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps> { +class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> { @computed get backgroundView() { let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", ""); return !backgroundLayout ? (null) : (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"} - isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); + isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { return this.backgroundView; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 644a8784c..85a12defa 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -23,6 +23,7 @@ interface MarqueeViewProps { selectDocuments: (docs: Doc[]) => void; removeDocument: (doc: Doc) => boolean; addLiveTextDocument: (doc: Doc) => void; + isSelected: () => boolean; } @observer @@ -33,12 +34,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _downX: number = 0; @observable _downY: number = 0; @observable _visible: boolean = false; + _commandExecuted = false; @action cleanupInteractions = (all: boolean = false) => { if (all) { - document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); + document.removeEventListener("pointermove", this.onPointerMove, true); } document.removeEventListener("keydown", this.marqueeCommand, true); this._visible = false; @@ -56,6 +58,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; + this._commandExecuted = false; PreviewCursor.Visible = false; if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { @@ -75,7 +78,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> if (!e.cancelBubble) { if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { - this._visible = true; + if (!this._commandExecuted) { + this._visible = true; + } e.stopPropagation(); e.preventDefault(); } @@ -129,22 +134,27 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action marqueeCommand = (e: KeyboardEvent) => { - if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { + if (this._commandExecuted) { + return; + } + if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") { + this._commandExecuted = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); let ink = Cast(this.props.container.props.Document.ink, InkField); if (ink) { this.marqueeInkDelete(ink.inkData); } - this.cleanupInteractions(true); + this.cleanupInteractions(false); e.stopPropagation(); } if (e.key === "c" || e.key === "r" || e.key === "e") { + this._commandExecuted = true; e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().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.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; d.page = -1; return d; }); @@ -157,7 +167,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps> panX: 0, panY: 0, borderRounding: e.key === "e" ? -1 : undefined, - backgroundColor: selected.length ? "white" : "", scale: zoomBasis, width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, @@ -166,7 +175,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> }); this.marqueeInkDelete(inkData); - SelectionManager.DeselectAll(); + // SelectionManager.DeselectAll(); if (e.key === "r") { let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); Doc.MakeLink(summary.proto!, newCollection.proto!); @@ -176,9 +185,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> else { this.props.addDocument(newCollection, false); } - this.cleanupInteractions(true); + this.cleanupInteractions(false); } if (e.key === "s") { + this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); let bounds = this.Bounds; @@ -188,7 +198,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.addLiveTextDocument(summary); selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!)); - this.cleanupInteractions(true); + this.cleanupInteractions(false); } } @action diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 8766eb7ea..56c2a80fa 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -103,7 +103,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF target.width = width; target.height = height; } - (target as any).isIconAnimating = false; + target.isIconAnimating = false; } }, 2); @@ -114,34 +114,34 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF let isMinimized: boolean | undefined; let minimizedDocSet = Cast(this.props.Document.linkTags, listSpec(Doc)); if (!minimizedDocSet) return; - minimizedDocSet.map(async minimizedDoc => { - if (minimizedDoc instanceof Document) { - this.props.addDocument && this.props.addDocument(minimizedDoc, false); - let maximizedDoc = await Cast(minimizedDoc.maximizedDoc, Doc); - if (maximizedDoc && !(maximizedDoc as any).isIconAnimating) { - (maximizedDoc as any).isIconAnimating = true; - if (isMinimized === undefined) { - let maximizedDocMinimizedState = Cast(maximizedDoc.isMinimized, "boolean"); - isMinimized = (maximizedDocMinimizedState) ? true : false; - } - let minx = NumCast(minimizedDoc.x, undefined); - let miny = NumCast(minimizedDoc.y, undefined); - let maxx = NumCast(maximizedDoc.x, undefined); - let maxy = NumCast(maximizedDoc.y, undefined); - let maxw = NumCast(maximizedDoc.width, undefined); - let maxh = NumCast(maximizedDoc.height, undefined); - if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && - maxw !== undefined && maxh !== undefined) { - this.animateBetweenIcon(true, [minx, miny], [maxx, maxy], maxw, maxh, Date.now(), maximizedDoc, isMinimized); - } + let docs = minimizedDocSet.map(d => d); + docs.push(this.props.Document); + docs.map(async minimizedDoc => { + this.props.addDocument && this.props.addDocument(minimizedDoc, false); + let maximizedDoc = await Cast(minimizedDoc.maximizedDoc, Doc); + if (maximizedDoc && !maximizedDoc.isIconAnimating) { + maximizedDoc.isIconAnimating = true; + if (isMinimized === undefined) { + let maximizedDocMinimizedState = Cast(maximizedDoc.isMinimized, "boolean"); + isMinimized = (maximizedDocMinimizedState) ? true : false; + } + let minx = NumCast(minimizedDoc.x, undefined); + let miny = NumCast(minimizedDoc.y, undefined); + let maxx = NumCast(maximizedDoc.x, undefined); + let maxy = NumCast(maximizedDoc.y, undefined); + let maxw = NumCast(maximizedDoc.width, undefined); + let maxh = NumCast(maximizedDoc.height, undefined); + if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && + maxw !== undefined && maxh !== undefined) { + this.animateBetweenIcon(true, [minx, miny], [maxx, maxy], maxw, maxh, Date.now(), maximizedDoc, isMinimized); } - } }) } onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; + e.stopPropagation(); } onClick = async (e: React.MouseEvent) => { e.stopPropagation(); @@ -175,7 +175,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF const screenWidth = 1800; let fadeUp = .75 * screenWidth; let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; - zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; return ( <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index c90ca6d17..794442469 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -37,8 +37,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); } CreateBindings(): JsxBindings { - let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; - return bindings; + return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; } render() { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3814eeb9c..eab068355 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,13 +12,14 @@ import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { Template, Templates } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); import { Opt, Doc } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, PromiseValue } from "../../../new_fields/Types"; +import { FieldValue, Cast, PromiseValue, StrCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; @@ -87,6 +88,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu public get ContentDiv() { return this._mainCont.current; } @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost(): boolean { return this.props.isTopMost; } + @computed get templates(): Array<Template> { + return new Array<Template>(); + // let field = this.props.Document[KeyStore.Templates]; + // return !field || field === FieldWaiting ? [] : field.Data; + } + set templates(templates: Array<Template>) { /* this.props.Document.templates = templates;*/ } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); @action componentDidMount() { @@ -151,7 +159,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - if (e.shiftKey && e.buttons === 2) { + if (e.shiftKey && e.buttons === 1) { if (this.props.isTopMost) { this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); } else { @@ -233,6 +241,46 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + updateLayout = async () => { + const baseLayout = await StrCast(this.props.Document.baseLayout); + if (baseLayout) { + let base = baseLayout; + let layout = baseLayout; + + this.templates.forEach(template => { + let temp = template.Layout; + layout = temp.replace("{layout}", base); + base = layout; + }); + + this.props.Document.layout = layout; + } + } + + @action + addTemplate = (template: Template) => { + let templates = this.templates; + templates.push(template); + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); + } + + @action + removeTemplate = (template: Template) => { + let templates = this.templates; + for (let i = 0; i < templates.length; i++) { + let temp = templates[i]; + if (temp.Name === template.Name) { + templates.splice(i, 1); + break; + } + } + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); + } + @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); @@ -260,8 +308,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); - @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } - @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); } render() { @@ -274,8 +322,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ref={this._mainCont} style={{ borderRadius: "inherit", - background: FieldValue(this.Document.backgroundColor) || "", - width: nativeWidth, height: nativeHeight, + background: this.Document.backgroundColor || "", + width: nativeWidth, + height: nativeHeight, transform: `scale(${scaling}, ${scaling})` }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index df76f7cea..c04b91c21 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -89,7 +89,7 @@ export class FieldView extends React.Component<FieldViewProps> { isTopMost={true} //TODO Why is this top most? selectOnLoad={false} focus={emptyFunction} - isSelected={returnFalse} + isSelected={this.props.isSelected} select={returnFalse} layoutKey={"layout"} ContainingCollectionView={this.props.ContainingCollectionView} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index f4f37250f..727d3c0b2 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -10,7 +10,7 @@ outline: none !important; } -.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { +.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { background: $light-color-secondary; padding: 0; border-width: 0px; @@ -24,10 +24,19 @@ height: 100%; pointer-events: all; } + .formattedTextBox-cont-hidden { overflow: hidden; pointer-events: none; } +.formattedTextBox-inner-rounded { + height: calc(100% - 40px); + width: calc(100% - 40px); + position: absolute; + overflow: scroll; + top: 20; + left: 20; +} .menuicon { display: inline-block; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index c4c720ca9..455672472 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, reaction, trace, computed } from "mobx"; +import { action, IReactionDisposer, reaction, trace, computed, _allowStateChangesInsideComputed } from "mobx"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; @@ -20,7 +20,7 @@ import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Opt, Doc } from "../../../new_fields/Doc"; import { observer } from "mobx-react"; import { InkingControl } from "../InkingControl"; -import { StrCast, Cast } from "../../../new_fields/Types"; +import { StrCast, Cast, NumCast } from "../../../new_fields/Types"; import { RichTextField } from "../../../new_fields/RichTextField"; import { Id } from "../../../new_fields/RefField"; const { buildMenuItems } = require("prosemirror-example-setup"); @@ -60,6 +60,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return FieldView.LayoutString(FormattedTextBox, fieldStr); } private _ref: React.RefObject<HTMLDivElement>; + private _proseRef: React.RefObject<HTMLDivElement>; private _editorView: Opt<EditorView>; private _gotDown: boolean = false; private _reactionDisposer: Opt<IReactionDisposer>; @@ -70,19 +71,26 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe super(props); this._ref = React.createRef(); + this._proseRef = React.createRef(); } _applyingChange: boolean = false; + _lastState: any = undefined; dispatchTransaction = (tx: Transaction) => { if (this._editorView) { - const state = this._editorView.state.apply(tx); + const state = this._lastState = this._editorView.state.apply(tx); this._editorView.updateState(state); this._applyingChange = true; Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON()))); Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n")); this._applyingChange = false; - // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); + let title = StrCast(this.props.Document.title); + if (title && title.startsWith("-") && this._editorView) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + this.props.Document.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + }; } } @@ -91,10 +99,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe schema, inpRules, //these currently don't do anything, but could eventually be helpful plugins: this.props.isOverlay ? [ + this.tooltipTextMenuPlugin(), history(), keymap(buildKeymap(schema)), keymap(baseKeymap), - this.tooltipTextMenuPlugin(), // this.tooltipLinkingMenuPlugin(), new Plugin({ props: { @@ -165,18 +173,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 1 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { - console.log("first"); + if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) + this._toolTipTextMenu.tooltip.style.opacity = "0"; } - if (e.button === 2) { + if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { this._gotDown = true; - console.log("second"); e.preventDefault(); } } onPointerUp = (e: React.PointerEvent): void => { - console.log("pointer up"); + if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) + this._toolTipTextMenu.tooltip.style.opacity = "1"; if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } @@ -184,9 +193,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onFocused = (e: React.FocusEvent): void => { if (!this.props.isOverlay) { - if (MainOverlayTextBox.Instance.TextDoc !== this.props.Document) { - MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform); - } + MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform); } else { if (this._ref.current) { this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; @@ -228,18 +235,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } onClick = (e: React.MouseEvent): void => { - this._ref.current!.focus(); + this._proseRef.current!.focus(); } tooltipTextMenuPlugin() { let myprops = this.props; + let self = this; return new Plugin({ view(_editorView) { - return new TooltipTextMenu(_editorView, myprops); + return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); } }); } + _toolTipTextMenu: TooltipTextMenu | undefined = undefined; tooltipLinkingMenuPlugin() { let myprops = this.props; return new Plugin({ @@ -254,7 +263,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe SelectionManager.DeselectAll(); } e.stopPropagation(); - if (e.keyCode === 9) e.preventDefault(); + if (e.key === "Tab") e.preventDefault(); // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; @@ -266,10 +275,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } render() { let style = this.props.isOverlay ? "scroll" : "hidden"; + let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : ""; let color = StrCast(this.props.Document.backgroundColor); let interactive = InkingControl.Instance.selectedTool ? "" : "interactive"; return ( - <div className={`formattedTextBox-cont-${style}`} + <div className={`formattedTextBox-cont${style}`} ref={this._ref} style={{ pointerEvents: interactive ? "all" : "none", background: color, @@ -283,7 +293,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onContextMenu={this.specificContextMenu} // tfs: do we need this event handler onWheel={this.onPointerWheel} - ref={this._ref} /> + > + <div className={`formattedTextBox-inner${rounded}`} ref={this._proseRef} /> + </div> ); } } diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index f7cceb3d4..7a0c49735 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -23,7 +23,7 @@ export class IconBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(IconBox); } @computed get maximized() { return Cast(this.props.Document.maximizedDoc, Doc); } - @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.layout : "<p>Error loading layout data</p>"; } + @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; } @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } public static DocumentIcon(layout: string) { diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 6ebd73f2c..20cae03d4 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -8,6 +8,7 @@ border-radius: $border-radius; box-sizing: border-box; display: inline-block; + pointer-events: all; .imageBox-cont img { width: auto; } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 01701e02c..ff6885965 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -25,4 +25,5 @@ } .keyValuePair-td-value { display:inline-block; + overflow: scroll; }
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 830dfe6c6..3760e378a 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -4,6 +4,9 @@ top: 0; left:0; } +.react-pdf__Page__textContent span { + user-select: text; +} .react-pdf__Document { position: absolute; } @@ -12,6 +15,21 @@ top: 0; left:0; z-index: 25; + pointer-events: all; +} +.pdfButton { + pointer-events: all; + width: 100px; + height:100px; +} +.pdfBox-cont { + pointer-events: none ; + span { + pointer-events: none !important; + } +} +.pdfBox-cont-interactive { + pointer-events: all; } .pdfBox-contentContainer { position: absolute; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 9f0849492..eb45ea273 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,5 +1,5 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; @@ -10,18 +10,17 @@ import { RouteStore } from "../../../server/RouteStore"; import { Utils } from '../../../Utils'; import { Annotation } from './Annotation'; import { FieldView, FieldViewProps } from './FieldView'; -import "./ImageBox.scss"; import "./PDFBox.scss"; -import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here import React = require("react"); import { SelectionManager } from "../../util/SelectionManager"; -import { Cast, FieldValue } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { Opt } from "../../../new_fields/Doc"; import { DocComponent } from "../DocComponent"; import { makeInterface } from "../../../new_fields/Schema"; import { positionSchema } from "./DocumentView"; import { pageSchema } from "./ImageBox"; import { ImageField, PdfField } from "../../../new_fields/URLField"; +import { InkingControl } from "../InkingControl"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -43,15 +42,6 @@ import { ImageField, PdfField } from "../../../new_fields/URLField"; * 4) another method: click on highlight first and then drag on your desired text * 5) To make another highlight, you need to reclick on the button * - * Draw: - * 1) click draw and select color. then just draw like there's no tomorrow. - * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. - * - * Pagination: - * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. - * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. - * - * * written by: Andrew Kim */ @@ -63,30 +53,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen public static LayoutString() { return FieldView.LayoutString(PDFBox); } private _mainDiv = React.createRef<HTMLDivElement>(); - private _pdf = React.createRef<HTMLCanvasElement>(); @observable private _renderAsSvg = true; - //very useful for keeping track of X and y position throughout the PDF Canvas - private initX: number = 0; - private initY: number = 0; - - //checks if tool is on - private _toolOn: boolean = false; //checks if tool is on - private _pdfContext: any = null; //gets pdf context - private bool: Boolean = false; //general boolean debounce - private currSpan: any;//keeps track of current span (for highlighting) - - private _currTool: any; //keeps track of current tool button reference - private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool - private _drawTool = React.createRef<HTMLButtonElement>();//drawing tool button reference - - private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference - private _currColor: string = "black"; //current color that user selected (for ink/pen) - - private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference - private _highlightToolOn: boolean = false; - private _pdfCanvas: any; private _reactionDisposer: Opt<IReactionDisposer>; @observable private _perPageInfo: Object[] = []; //stores pageInfo @@ -119,43 +88,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } /** - * selection tool used for area highlighting (stickies). Kinda temporary - */ - selectionTool = () => { - this._toolOn = true; - } - /** - * when user draws on the canvas. When mouse pointer is down - */ - drawDown = (e: PointerEvent) => { - this.initX = e.offsetX; - this.initY = e.offsetY; - this._pdfContext.beginPath(); - this._pdfContext.lineTo(this.initX, this.initY); - this._pdfContext.strokeStyle = this._currColor; - this._pdfCanvas.addEventListener("pointermove", this.drawMove); - this._pdfCanvas.addEventListener("pointerup", this.drawUp); - - } - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = this.initX += e.movementX, - y = this.initY += e.movementY; - //connects the point - this._pdfContext.lineTo(x, y); - this._pdfContext.stroke(); - } - - drawUp = (e: PointerEvent) => { - this._pdfContext.closePath(); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - } - - - /** * highlighting helper function */ makeEditableAndHighlight = (colour: string) => { @@ -190,7 +122,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen child.id = "highlighted"; //@ts-ignore obj.spans.push(child); - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } }); } @@ -213,7 +145,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen child.id = "highlighted"; //@ts-ignore temp.spans.push(child); - child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler } }); @@ -279,11 +211,20 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen * controls the area highlighting (stickies) Kinda temporary */ onPointerDown = (e: React.PointerEvent) => { - if (this._toolOn) { - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - + if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) { + if (e.altKey) { + this._alt = true; + } else { + if (e.metaKey) + e.stopPropagation(); + } + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + if (this.props.isSelected() && e.buttons === 2) { + this._alt = true; + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); } } @@ -291,96 +232,15 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen * controls area highlighting and partially highlighting. Kinda temporary */ @action - onPointerUp = (e: React.PointerEvent) => { - if (this._highlightToolOn) { + onPointerUp = (e: PointerEvent) => { + this._alt = false; + document.removeEventListener("pointerup", this.onPointerUp); + if (this.props.isSelected()) { this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. - this._highlightToolOn = false; - } - if (this._toolOn) { - let mouse = e.nativeEvent; - let finalX = mouse.offsetX; - let finalY = mouse.offsetY; - let width = Math.abs(finalX - this.initX); //width - let height = Math.abs(finalY - this.initY); //height - - //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky - if (finalX < this.initX) { - this.initX = finalX; - } - if (finalY < this.initY) { - this.initY = finalY; - } - - if (this._mainDiv.current) { - let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} />; - this._pageInfo.area.push(sticky); - } - this._toolOn = false; } this._interactive = true; } - /** - * starts drawing the line when user presses down. - */ - onDraw = () => { - if (this._currTool !== null) { - this._currTool.style.backgroundColor = "grey"; - } - - if (this._drawTool.current) { - this._currTool = this._drawTool.current; - if (this._drawToolOn) { - this._drawToolOn = false; - this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); - this._pdfCanvas.removeEventListener("pointerup", this.drawUp); - this._pdfCanvas.removeEventListener("pointermove", this.drawMove); - this._drawTool.current.style.backgroundColor = "grey"; - } else { - this._drawToolOn = true; - this._pdfCanvas.addEventListener("pointerdown", this.drawDown); - this._drawTool.current.style.backgroundColor = "cyan"; - } - } - } - - - /** - * for changing color (for ink/pen) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML === "Red") { - this._currColor = "red"; - } else if (e.currentTarget.innerHTML === "Blue") { - this._currColor = "blue"; - } else if (e.currentTarget.innerHTML === "Green") { - this._currColor = "green"; - } else if (e.currentTarget.innerHTML === "Black") { - this._currColor = "black"; - } - - } - - - /** - * For highlighting (text drag highlighting) - */ - onHighlight = () => { - this._drawToolOn = false; - if (this._currTool !== null) { - this._currTool.style.backgroundColor = "grey"; - } - if (this._highlightTool.current) { - this._currTool = this._drawTool.current; - if (this._highlightToolOn) { - this._highlightToolOn = false; - this._highlightTool.current.style.backgroundColor = "grey"; - } else { - this._highlightToolOn = true; - this._highlightTool.current.style.backgroundColor = "orange"; - } - } - } @action @@ -403,22 +263,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action onLoaded = (page: any) => { - if (this._mainDiv.current) { - this._mainDiv.current.childNodes.forEach((element) => { - if (element.nodeName === "DIV") { - element.childNodes[0].childNodes.forEach((e) => { - - if (e instanceof HTMLCanvasElement) { - this._pdfCanvas = e; - this._pdfContext = e.getContext("2d"); - - } - - }); - } - }); - } - // bcz: the number of pages should really be set when the document is imported. this.props.Document.numPages = page._transport.numPages; if (this._perPageInfo.length === 0) { //Makes sure it only runs once @@ -430,7 +274,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @action setScaling = (r: any) => { // bcz: the nativeHeight should really be set when the document is imported. - // also, the native dimensions could be different for different pages of the PDF + // also, the native dimensions could be different for different pages of the canvas // so this design is flawed. var nativeWidth = FieldValue(this.Document.nativeWidth, 0); if (!FieldValue(this.Document.nativeHeight, 0)) { @@ -439,22 +283,29 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen this.props.Document.nativeHeight = nativeHeight; } } - + renderHeight = 2400; + @computed + get pdfPage() { + return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} /> + } @computed get pdfContent() { let page = this.curPage; const renderHeight = 2400; let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight; + let body = NumCast(this.props.Document.nativeHeight) ? + this.pdfPage : + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + {this.pdfPage} + </div> + } + </Measure>; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl!.url.href}`} renderMode={this._renderAsSvg ? "svg" : ""}> - <Measure onResize={this.setScaling}> - {({ measureRef }) => - <div className="pdfBox-page" ref={measureRef}> - <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> - </div> - } - </Measure> + <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}> + {body} </Document> </div >; } @@ -484,10 +335,24 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } return (null); } - + @observable _alt = false; + @action + onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Alt") { + this._alt = true; + } + } + @action + onKeyUp = (e: React.KeyboardEvent) => { + if (e.key === "Alt") { + this._alt = false; + } + } render() { + trace(); + let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( - <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} > {this.pdfRenderer} </div > ); diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx deleted file mode 100644 index 11719831b..000000000 --- a/src/client/views/nodes/Sticky.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import "react-image-lightbox/style.css"; // This only needs to be imported once in your app -import React = require("react"); -import { observer } from "mobx-react"; -import "react-pdf/dist/Page/AnnotationLayer.css"; - -interface IProps { - Height: number; - Width: number; - X: number; - Y: number; -} - -/** - * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. - * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. - * - * Written By: Andrew Kim - */ -@observer -export class Sticky extends React.Component<IProps> { - private initX: number = 0; - private initY: number = 0; - - private _ref = React.createRef<HTMLCanvasElement>(); - private ctx: any; //context that keeps track of sticky canvas - - /** - * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas - */ - drawDown = (e: React.PointerEvent) => { - if (this._ref.current) { - this.ctx = this._ref.current.getContext("2d"); - let mouse = e.nativeEvent; - this.initX = mouse.offsetX; - this.initY = mouse.offsetY; - this.ctx.beginPath(); - this.ctx.lineTo(this.initX, this.initY); - this.ctx.strokeStyle = "black"; - document.addEventListener("pointermove", this.drawMove); - document.addEventListener("pointerup", this.drawUp); - } - } - - //when user drags - drawMove = (e: PointerEvent): void => { - //x and y mouse movement - let x = (this.initX += e.movementX), - y = (this.initY += e.movementY); - //connects the point - this.ctx.lineTo(x, y); - this.ctx.stroke(); - } - - /** - * when user lifts the mouse, the drawing ends - */ - drawUp = (e: PointerEvent) => { - this.ctx.closePath(); - console.log(this.ctx); - document.removeEventListener("pointermove", this.drawMove); - } - - render() { - return ( - <div onPointerDown={this.drawDown}> - <canvas - ref={this._ref} - height={this.props.Height} - width={this.props.Width} - style={{ - position: "absolute", - top: "20px", - left: "0px", - zIndex: 1, - background: "yellow", - transform: `translate(${this.props.X}px, ${this.props.Y}px)`, - opacity: 0.4 - }} - /> - </div> - ); - } -} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 86276660a..184a89dbc 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -7,7 +7,7 @@ import { DocComponent } from "../DocComponent"; import { positionSchema } from "./DocumentView"; import { makeInterface } from "../../../new_fields/Schema"; import { pageSchema } from "./ImageBox"; -import { Cast, FieldValue } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import Measure from "react-measure"; import "./VideoBox.scss"; @@ -59,24 +59,27 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD (vref as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage; } } + videoContent(path: string) { + return <video className="videobox-cont" ref={this.setVideoRef}> + <source src={path} type="video/mp4" /> + Not supported. + </video>; + } render() { let field = FieldValue(Cast(this.Document[this.props.fieldKey], VideoField)); if (!field) { return <div>Loading</div>; } - let path = field.url.href; - return ( + let content = this.videoContent(field.url.href); + return NumCast(this.props.Document.nativeHeight) ? + content : <Measure onResize={this.setScaling}> {({ measureRef }) => <div style={{ width: "100%", height: "auto" }} ref={measureRef}> - <video className="videobox-cont" ref={this.setVideoRef}> - <source src={path} type="video/mp4" /> - Not supported. - </video> + {content} </div> } - </Measure> - ); + </Measure>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 2ad1129a4..eb09b0693 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,12 +1,19 @@ -.webBox-cont { +.webBox-cont, .webBox-cont-interactive{ padding: 0vw; position: absolute; top: 0; left:0; width: 100%; height: 100%; - overflow: scroll; + overflow: auto; + pointer-events: none ; +} +.webBox-cont-interactive { + pointer-events: all; + span { + user-select: text !important; + } } #webBox-htmlSpan { @@ -15,6 +22,12 @@ left:0; } +.webBox-overlay { + width: 100%; + height: 100%; + position: absolute; +} + .webBox-button { padding : 0vw; border: none; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index c12fd5655..2239a8e38 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -6,6 +6,7 @@ import { WebField } from "../../../new_fields/URLField"; import { observer } from "mobx-react"; import { computed, reaction, IReactionDisposer } from 'mobx'; import { DocumentDecorations } from "../DocumentDecorations"; +import { InkingControl } from "../InkingControl"; @observer export class WebBox extends React.Component<FieldViewProps> { @@ -46,12 +47,13 @@ export class WebBox extends React.Component<FieldViewProps> { let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : ""); return ( <> - <div className="webBox-cont" > + <div className={classname} > {content} </div> - {!frozen ? (null) : <div onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} style={{ width: "100%", height: "100%", position: "absolute" }} />} + {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />} </>); } }
\ No newline at end of file diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx index 7415d4b28..04ef00722 100644 --- a/src/debug/Test.tsx +++ b/src/debug/Test.tsx @@ -61,14 +61,12 @@ class Test extends React.Component { assert(test2.testDoc === undefined); test2.url = 35; assert(test2.url === 35); - const l = new List<number>(); + const l = new List<Doc>(); //TODO push, and other array functions don't go through the proxy - l.push(1); + l.push(doc2); //TODO currently length, and any other string fields will get serialized - l.length = 3; - l[2] = 5; + doc.list = l; console.log(l.slice()); - console.log(SerializationHelper.Serialize(l)); } render() { diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts new file mode 100644 index 000000000..72ae13c2e --- /dev/null +++ b/src/fields/TemplateField.ts @@ -0,0 +1,43 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; +import { Template, TemplatePosition } from "../client/views/Templates"; + + +export class TemplateField extends BasicField<Array<Template>> { + constructor(data: Array<Template> = [], id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new TemplateField("${this.Data}")`; + } + + Copy() { + return new TemplateField(this.Data); + } + + ToJson() { + let templates: Array<{ name: string, position: TemplatePosition, layout: string }> = []; + this.Data.forEach(template => { + templates.push({ name: template.Name, layout: template.Layout, position: template.Position }); + }); + return { + type: Types.Templates, + data: templates, + id: this.Id, + }; + } + + UpdateFromServer(data: any) { + this.data = new Array(data); + } + + static FromJson(id: string, data: any): TemplateField { + let templates: Array<Template> = []; + data.forEach((template: { name: string, position: number, layout: string }) => { + templates.push(new Template(template.name, template.position, template.layout)); + }); + return new TemplateField(templates, id, false); + } +}
\ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index d15b6309d..0e5105761 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -3,8 +3,8 @@ import { serializable, primitive, map, alias, list } from "serializr"; import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper"; import { Utils } from "../Utils"; import { DocServer } from "../client/DocServer"; -import { setter, getter, getField, updateFunction } from "./util"; -import { Cast, ToConstructor, PromiseValue, FieldValue } from "./Types"; +import { setter, getter, getField, updateFunction, deleteProperty } from "./util"; +import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types"; import { UndoManager, undoBatch } from "../client/util/UndoManager"; import { listSpec } from "./Schema"; import { List } from "./List"; @@ -25,6 +25,8 @@ export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract export const Update = Symbol("Update"); export const Self = Symbol("Self"); +export const WidthSym = Symbol("Width"); +export const HeightSym = Symbol("Height"); @Deserializable("doc").withFields(["id"]) export class Doc extends RefField { @@ -34,7 +36,7 @@ export class Doc extends RefField { set: setter, get: getter, ownKeys: target => Object.keys(target.__fields), - deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + deleteProperty: deleteProperty, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); if (!id || forceSave) { @@ -70,6 +72,8 @@ export class Doc extends RefField { } private [Self] = this; + public [WidthSym] = () => { return NumCast(this.__fields.width); } // bcz: is this the right way to access width/height? it didn't work with : this.width + public [HeightSym] = () => { return NumCast(this.__fields.height); } } export namespace Doc { @@ -151,8 +155,8 @@ export namespace Doc { } export function MakeLink(source: Doc, target: Doc): Doc { - let linkDoc = new Doc; - UndoManager.RunInBatch(() => { + return UndoManager.RunInBatch(() => { + let linkDoc = new Doc; linkDoc.title = "New Link"; linkDoc.linkDescription = ""; linkDoc.linkTags = "Default"; @@ -171,8 +175,8 @@ export namespace Doc { source.linkedToDocs = linkedTo = new List<Doc>(); } linkedTo.push(linkDoc); + return linkDoc; }, "make link"); - return linkDoc; } export function MakeDelegate(doc: Doc): Doc; diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts index 46f111f8e..c79a2f79a 100644 --- a/src/new_fields/IconField.ts +++ b/src/new_fields/IconField.ts @@ -5,10 +5,10 @@ import { ObjectField } from "./ObjectField"; @Deserializable("icon") export class IconField extends ObjectField { @serializable(primitive()) - readonly layout: string; + readonly icon: string; - constructor(layout: string) { + constructor(icon: string) { super(); - this.layout = layout; + this.icon = icon; } } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index e4a80f7a1..ec1bf44a9 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -1,21 +1,173 @@ import { Deserializable, autoObject } from "../client/util/SerializationHelper"; import { Field, Update, Self } from "./Doc"; -import { setter, getter } from "./util"; +import { setter, getter, deleteProperty } from "./util"; import { serializable, alias, list } from "serializr"; import { observable, observe, IArrayChange, IArraySplice, IObservableArray, Lambda, reaction } from "mobx"; import { ObjectField, OnUpdate } from "./ObjectField"; +import { RefField } from "./RefField"; +import { ProxyField } from "./Proxy"; const listHandlers: any = { - push(...items: any[]) { - // console.log("push"); - // console.log(...items); - return this[Self].__fields.push(...items); + /// Mutator methods + copyWithin() { + throw new Error("copyWithin not supported yet"); + }, + fill(value: any, start?: number, end?: number) { + if (value instanceof RefField) { + throw new Error("fill with RefFields not supported yet"); + } + const res = this[Self].__fields.fill(value, start, end); + this[Update](); + return res; }, pop(): any { - return this[Self].__fields.pop(); + const field = toRealField(this[Self].__fields.pop()); + this[Update](); + return field; + }, + push(...items: any[]) { + items = items.map(toObjectField); + const res = this[Self].__fields.push(...items); + this[Update](); + return res; + }, + reverse() { + const res = this[Self].__fields.reverse(); + this[Update](); + return res; + }, + shift() { + const res = toRealField(this[Self].__fields.shift()); + this[Update](); + return res; + }, + sort(cmpFunc: any) { + const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + this[Update](); + return res; + }, + splice(start: number, deleteCount: number, ...items: any[]) { + items = items.map(toObjectField); + const res = this[Self].__fields.splice(start, deleteCount, ...items); + this[Update](); + return res.map(toRealField); + }, + unshift(...items: any[]) { + items = items.map(toObjectField); + const res = this[Self].__fields.unshift(...items); + this[Update](); + return res; + + }, + /// Accessor methods + concat(...items: any[]) { + return this[Self].__fields.map(toRealField).concat(...items); + }, + includes(valueToFind: any, fromIndex: number) { + const fields = this[Self].__fields; + if (valueToFind instanceof RefField) { + return fields.map(toRealField).includes(valueToFind, fromIndex); + } else { + return fields.includes(valueToFind, fromIndex); + } + }, + indexOf(valueToFind: any, fromIndex: number) { + const fields = this[Self].__fields; + if (valueToFind instanceof RefField) { + return fields.map(toRealField).indexOf(valueToFind, fromIndex); + } else { + return fields.indexOf(valueToFind, fromIndex); + } + }, + join(separator: any) { + return this[Self].__fields.map(toRealField).join(separator); + }, + lastIndexOf(valueToFind: any, fromIndex: number) { + const fields = this[Self].__fields; + if (valueToFind instanceof RefField) { + return fields.map(toRealField).lastIndexOf(valueToFind, fromIndex); + } else { + return fields.lastIndexOf(valueToFind, fromIndex); + } + }, + slice(begin: number, end: number) { + return this[Self].__fields.slice(begin, end).map(toRealField); + }, + + /// Iteration methods + entries() { + return this[Self].__fields.map(toRealField).entries(); + }, + every(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).every(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + filter(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).filter(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + find(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).find(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + findIndex(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).findIndex(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + forEach(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).forEach(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + map(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).map(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + reduce(callback: any, initialValue: any) { + return this[Self].__fields.map(toRealField).reduce(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + reduceRight(callback: any, initialValue: any) { + return this[Self].__fields.map(toRealField).reduceRight(callback, initialValue); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + }, + some(callback: any, thisArg: any) { + return this[Self].__fields.map(toRealField).some(callback, thisArg); + // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. + // If we don't want to support the array parameter, we should use this version instead + // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + }, + values() { + return this[Self].__fields.map(toRealField).values(); + }, + [Symbol.iterator]() { + return this[Self].__fields.map(toRealField).values(); } }; +function toObjectField(field: Field) { + return field instanceof RefField ? new ProxyField(field) : field; +} + +function toRealField(field: Field) { + return field instanceof ProxyField ? field.value() : field; +} + function listGetter(target: any, prop: string | number | symbol, receiver: any): any { if (listHandlers.hasOwnProperty(prop)) { return listHandlers[prop]; @@ -38,36 +190,15 @@ interface ListIndexUpdate<T> { type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>; -const ObserveDisposer = Symbol("Observe Disposer"); - -function listObserver<T extends Field>(this: ListImpl<T>, change: IArrayChange<T> | IArraySplice<T>) { - if (change.type === "splice") { - this[Update]({ - index: change.index, - removedCount: change.removedCount, - added: change.added, - type: change.type - }); - } else { - //This should already be handled by the getter for the Proxy - // this[Update]({ - // index: change.index, - // newValue: change.newValue, - // type: change.type - // }); - } -} - @Deserializable("list") class ListImpl<T extends Field> extends ObjectField { constructor(fields: T[] = []) { super(); this.___fields = fields; - this[ObserveDisposer] = observe(this.__fields as IObservableArray<T>, listObserver.bind(this)); const list = new Proxy<this>(this, { set: setter, - get: getter, - deleteProperty: () => { throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); }, + get: listGetter, + deleteProperty: deleteProperty, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); return list; @@ -82,8 +213,6 @@ class ListImpl<T extends Field> extends ObjectField { private set __fields(value) { this.___fields = value; - this[ObserveDisposer](); - this[ObserveDisposer] = observe(this.__fields as IObservableArray<T>, listObserver.bind(this)); } // @serializable(alias("fields", list(autoObject()))) @@ -97,7 +226,6 @@ class ListImpl<T extends Field> extends ObjectField { update && update(); } - private [ObserveDisposer]: Lambda; private [Self] = this; } export type List<T extends Field> = ListImpl<T> & T[]; diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index 511820115..128817ab8 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -78,6 +78,14 @@ export function getField(target: any, prop: string | number, ignoreProto: boolea return field; } +export function deleteProperty(target: any, prop: string | number | symbol) { + if (typeof prop === "symbol") { + delete target[prop]; + return true; + } + throw new Error("Currently properties can't be deleted from documents, assign to undefined instead"); +} + export function updateFunction(target: any, prop: any, value: any) { return (diff?: any) => { if (!diff) diff = { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } }; diff --git a/src/server/Message.ts b/src/server/Message.ts index 81da44f72..e9a8b0f0c 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -15,7 +15,7 @@ export class Message<T> { export enum Types { Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference, - Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, + Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, Templates } export interface Transferable { |