diff options
Diffstat (limited to 'src/client/views/collections')
15 files changed, 573 insertions, 352 deletions
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index 9191bf822..77af2dc0e 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -1,9 +1,9 @@ -import { action, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { makeInterface } from '../../../new_fields/Schema'; -import { BoolCast, NumCast, StrCast } from '../../../new_fields/Types'; +import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types'; import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { Transform } from '../../util/Transform'; @@ -13,6 +13,7 @@ import { CollectionSubView } from './CollectionSubView'; import { DocumentView } from '../nodes/DocumentView'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { Id } from '../../../new_fields/FieldSymbols'; +import { ScriptField } from '../../../new_fields/ScriptField'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -21,12 +22,18 @@ const LinearDocument = makeInterface(documentSchema); @observer export class CollectionLinearView extends CollectionSubView(LinearDocument) { @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; private _dropDisposer?: DragManager.DragDropDisposer; private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; componentWillUnmount() { this._dropDisposer && this._dropDisposer(); this._widthDisposer && this._widthDisposer(); + this._selectedDisposer && this._selectedDisposer(); + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + }); } componentDidMount() { @@ -35,8 +42,29 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10), { fireImmediately: true } ); + + this._selectedDisposer = reaction( + () => NumCast(this.props.Document.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log); + } + }), + { fireImmediately: true } + ); } - protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this._dropDisposer && this._dropDisposer(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); @@ -55,13 +83,13 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { render() { const guid = Utils.GenerateGuid(); return <div className="collectionLinearView-outer"> - <div className="collectionLinearView" ref={this.createDropTarget} > + <div className="collectionLinearView" ref={this.createDashEventsTarget} > <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle} onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} /> <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label> <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}> - {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => { + {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => { const nested = pair.layout.viewType === CollectionViewType.Linear; const dref = React.createRef<HTMLDivElement>(); const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension()); diff --git a/src/client/views/collections/CollectionMulticolumnView.scss b/src/client/views/collections/CollectionMulticolumnView.scss deleted file mode 100644 index a54af748b..000000000 --- a/src/client/views/collections/CollectionMulticolumnView.scss +++ /dev/null @@ -1,23 +0,0 @@ -.collectionMulticolumnView_contents { - display: flex; - width: 100%; - height: 100%; - overflow: hidden; - - .document-wrapper { - display: flex; - flex-direction: column; - - .display { - text-align: center; - height: 20px; - } - - } - - .resizer { - background: black; - cursor: ew-resize; - } - -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx index 6af7cce70..53ad433b3 100644 --- a/src/client/views/collections/CollectionPivotView.tsx +++ b/src/client/views/collections/CollectionPivotView.tsx @@ -20,7 +20,7 @@ import { Set } from "typescript-collections"; export class CollectionPivotView extends CollectionSubView(doc => doc) { componentDidMount = () => { this.props.Document.freeformLayoutEngine = "pivot"; - if (!this.props.Document.facetCollection) { + if (true || !this.props.Document.facetCollection) { const facetCollection = Docs.Create.FreeformDocument([], { title: "facetFilters", yMargin: 0, treeViewHideTitle: true }); facetCollection.target = this.props.Document; @@ -34,7 +34,7 @@ export class CollectionPivotView extends CollectionSubView(doc => doc) { facetCollection.onCheckedClick = new ScriptField(script); } - const openDocText = "const alias = getAlias(this); alias.layoutKey = 'layoutCustom'; useRightSplit(alias); "; + const openDocText = "const alias = getAlias(this); alias.layoutKey = 'layout_detailed'; useRightSplit(alias); "; const openDocScript = CompileScript(openDocText, { params: { this: Doc.name, heading: "boolean", checked: "boolean", context: Doc.name }, typecheck: false, diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index cb95dcbbc..8b3d332af 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -15,6 +15,11 @@ display: flex; justify-content: space-between; flex-wrap: nowrap; + touch-action: none; + + div { + touch-action: none; + } .collectionSchemaView-tableContainer { @@ -49,7 +54,7 @@ .rt-table { height: 100%; display: -webkit-inline-box; - direction: ltr; + direction: ltr; overflow: visible; } @@ -202,7 +207,7 @@ button.add-column { border-bottom: 1px solid lightgray; &:first-child { - padding-top : 0; + padding-top: 0; } &:last-child { @@ -231,7 +236,7 @@ button.add-column { transition: background-color 0.2s; &:hover { - background-color: $light-color-secondary; + background-color: $light-color-secondary; } &.active { @@ -267,7 +272,7 @@ button.add-column { overflow-y: scroll; position: absolute; top: 28px; - box-shadow: 0 10px 16px rgba(0,0,0,0.1); + box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); .key-option { background-color: $light-color; @@ -380,6 +385,7 @@ button.add-column { &.editing { padding: 0; + input { outline: 0; border: none; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index ca792c134..7fe42386a 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -148,7 +148,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDropTarget(ele!); //so the whole grid is the drop target? + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } overlays = (doc: Doc) => { @@ -302,7 +302,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} />; } @@ -337,7 +337,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { docList={docList} parent={this} type={type} - createDropTarget={this.createDropTarget} + createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.props.ScreenToLocalTransform} setDocHeight={this.setDocHeight} />; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 39b4e4e1d..23a664359 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -18,6 +18,9 @@ import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; import "./CollectionStackingView.scss"; import { TraceMobx } from "../../../new_fields/util"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageField } from "../../../new_fields/URLField"; +import { ImageBox } from "../nodes/ImageBox"; library.add(faPalette); @@ -132,6 +135,15 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action addDocument = (value: string, shiftDown?: boolean) => { + if (value === ":freeForm") { + return this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { width: 200, height: 200 })); + } else if (value.startsWith(":")) { + const { Document, addDocument } = this.props.parent.props; + const fieldKey = value.substring(1); + const proto = Doc.GetProto(Document); + const created = Docs.Get.DocumentFromField(Document, fieldKey, proto); + return created ? addDocument(created) : false; + } this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document.sectionFilter); const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5753dd34e..5f4ee3669 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -23,6 +23,8 @@ import { basename } from 'path'; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { Networking } from "../../Network"; +import { GestureUtils } from "../../../pen-gestures/GestureUtils"; +import { InteractionUtils } from "../../util/InteractionUtils"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc) => boolean; @@ -47,15 +49,21 @@ export interface SubCollectionViewProps extends CollectionViewProps { export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; + private gestureDisposer?: GestureUtils.GestureEventDisposer; + protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; private _childLayoutDisposer?: IReactionDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer && this.dropDisposer(); + this.gestureDisposer && this.gestureDisposer(); + this.multiTouchDisposer && this.multiTouchDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); + this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); + this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view - this.createDropTarget(ele); + this.createDashEventsTarget(ele); } componentDidMount() { @@ -132,6 +140,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } @undoBatch + protected onGesture(e: Event, ge: GestureUtils.GestureEvent) { + + } + + @undoBatch @action protected drop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 0b9dc2eb2..2fa6813d7 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -7,9 +7,9 @@ border-radius: inherit; box-sizing: border-box; height: 100%; - width:100%; + width: 100%; position: relative; - top:0; + top: 0; padding-left: 10px; padding-right: 10px; background: $light-color-secondary; @@ -17,6 +17,7 @@ overflow: auto; user-select: none; cursor: default; + touch-action: pan-y; ul { list-style: none; @@ -115,6 +116,7 @@ .treeViewItem-header { border: transparent 1px solid; display: flex; + .editableView-container-editing-oneLine { min-width: 15px; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 70860b6bd..a48208bd9 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -29,6 +29,9 @@ import "./CollectionTreeView.scss"; import React = require("react"); import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { ScriptBox } from '../ScriptBox'; +import { ImageBox } from '../nodes/ImageBox'; +import { makeTemplate } from '../../util/DropConverter'; +import { CollectionDockingView } from './CollectionDockingView'; export interface TreeViewProps { @@ -628,9 +631,35 @@ export class CollectionTreeView extends CollectionSubView(Document) { layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" }); ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" }); } + ContextMenu.Instance.addItem({ + description: "Buxton Layout", icon: "eye", event: () => { + const { TextDocument, ImageDocument } = Docs.Create; + const wrapper = Docs.Create.StackingDocument([ + ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "hero" }), + TextDocument({ title: "year" }), + TextDocument({ title: "degrees_of_freedom" }), + TextDocument({ title: "company" }), + TextDocument({ title: "short_description" }), + ], { autoHeight: true, chromeStatus: "disabled" }); + wrapper.disableLOD = true; + makeTemplate(wrapper, true); + const detailedLayout = Doc.MakeAlias(wrapper); + const cardLayout = ImageBox.LayoutString("hero"); + this.childLayoutPairs.forEach(({ layout }) => { + const proto = Doc.GetProto(layout); + proto.layout = cardLayout; + proto.layout_detailed = detailedLayout; + layout.showTitle = "title"; + layout.showTitleHover = "titlehover"; + }); + } + }); const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean" }) }); + onClicks.push({ + description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document, + "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", context: Doc.name }) + }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 4bd456233..a665b678b 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,7 +1,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx'; import { observer } from "mobx-react"; import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; @@ -10,7 +10,7 @@ import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/FieldSymbols'; import { listSpec } from '../../../new_fields/Schema'; -import { BoolCast, Cast, StrCast } from '../../../new_fields/Types'; +import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { TraceMobx } from '../../../new_fields/util'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; @@ -27,7 +27,7 @@ import { CollectionDockingView } from "./CollectionDockingView"; import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionLinearView } from './CollectionLinearView'; -import { CollectionMulticolumnView } from './CollectionMulticolumnView'; +import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionPivotView } from './CollectionPivotView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; @@ -50,7 +50,8 @@ export enum CollectionViewType { Pivot, Linear, Staff, - Multicolumn + Multicolumn, + Timeline } export namespace CollectionViewType { @@ -90,8 +91,18 @@ export class CollectionView extends Touchable<FieldViewProps> { @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } + @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); } + @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); } + get collectionViewType(): CollectionViewType | undefined { - const viewField = Cast(this.props.Document.viewType, "number"); + if (!this.extensionDoc) return CollectionViewType.Invalid; + NumCast(this.props.Document.viewType) && setTimeout(() => { + if (this.props.Document.viewType) { + this.extensionDoc!.viewType = NumCast(this.props.Document.viewType); + } + Doc.GetProto(this.props.Document).viewType = this.props.Document.viewType = undefined; + }); + const viewField = NumCast(this.extensionDoc.viewType, Cast(this.props.Document.viewType, "number")); if (CollectionView._safeMode) { if (viewField === CollectionViewType.Freeform) { return CollectionViewType.Tree; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 7985e541f..01b978c81 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -26,7 +26,6 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { ContextMenu } from "../../ContextMenu"; import { ContextMenuProps } from "../../ContextMenuItem"; import { InkingControl } from "../../InkingControl"; -import { CreatePolyline } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/FormattedTextBox"; @@ -44,6 +43,7 @@ import { TraceMobx } from "../../../../new_fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; import { LinkManager } from "../../../util/LinkManager"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; +import CollectionPaletteVIew from "../../Palette"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -274,11 +274,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return clusterColor; } - @observable private _points: { X: number, Y: number }[] = []; @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + return; + } this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { document.removeEventListener("pointermove", this.onPointerMove); @@ -286,14 +287,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); // if physically using a pen or we're in pen or highlighter mode - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { - e.stopPropagation(); - e.preventDefault(); - const point = this.getTransform().transformPoint(e.pageX, e.pageY); - this._points.push({ X: point[0], Y: point[1] }); - } + // if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(e.pageX, e.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } // if not using a pen and in no ink mode - else if (InkingControl.Instance.selectedTool === InkTool.None) { + if (InkingControl.Instance.selectedTool === InkTool.None) { this._lastX = e.pageX; this._lastY = e.pageY; } @@ -325,106 +326,79 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } @action - handle1PointerDown = (e: React.TouchEvent) => { - const pt = e.targetTouches.item(0); - if (pt) { - this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; - if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { - e.stopPropagation(); - e.preventDefault(); - const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); - this._points.push({ X: point[0], Y: point[1] }); - } - else if (InkingControl.Instance.selectedTool === InkTool.None) { - this._lastX = pt.pageX; - this._lastY = pt.pageY; - e.stopPropagation(); - e.preventDefault(); - } - else { - e.stopPropagation(); - e.preventDefault(); + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { + if (!e.nativeEvent.cancelBubble) { + // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt = me.changedTouches[0]; + if (pt) { + this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false; + if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) { + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // e.stopPropagation(); + // e.preventDefault(); + // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); + // this._points.push({ X: point[0], Y: point[1] }); + // } + if (InkingControl.Instance.selectedTool === InkTool.None) { + this._lastX = pt.pageX; + this._lastY = pt.pageY; + e.preventDefault(); + } + else { + e.preventDefault(); + } } } } } - @action - onPointerUp = (e: PointerEvent): void => { - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && this._points.length <= 1) return; - - if (this._points.length > 1) { - const B = this.svgBounds; - const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); - - const result = GestureUtils.GestureRecognizer.Recognize(new Array(points)); - let actionPerformed = false; - if (result && result.Score > 0.7) { - switch (result.Name) { - case GestureUtils.Gestures.Box: - const bounds = { x: Math.min(...this._points.map(p => p.X)), r: Math.max(...this._points.map(p => p.X)), y: Math.min(...this._points.map(p => p.Y)), b: Math.max(...this._points.map(p => p.Y)) }; - const sel = this.getActiveDocuments().filter(doc => { - const l = NumCast(doc.x); - const r = l + doc[WidthSym](); - const t = NumCast(doc.y); - const b = t + doc[HeightSym](); - const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); - if (pass) { - doc.x = l - B.left - B.width / 2; - doc.y = t - B.top - B.height / 2; - } - return pass; - }); - this.addDocument(Docs.Create.FreeformDocument(sel, { x: B.left, y: B.top, width: B.width, height: B.height, panX: 0, panY: 0 })); - sel.forEach(d => this.props.removeDocument(d)); - actionPerformed = true; - break; - case GestureUtils.Gestures.Line: - const ep1 = this._points[0]; - const ep2 = this._points[this._points.length - 1]; - let d1: Doc | undefined; - let d2: Doc | undefined; - this.getActiveDocuments().map(doc => { - const l = NumCast(doc.x); - const r = l + doc[WidthSym](); - const t = NumCast(doc.y); - const b = t + doc[HeightSym](); - if (!d1 && l < ep1.X && r > ep1.X && t < ep1.Y && b > ep1.Y) { - d1 = doc; - } - else if (!d2 && l < ep2.X && r > ep2.X && t < ep2.Y && b > ep2.Y) { - d2 = doc; - } - }); - if (d1 && d2) { - if (!LinkManager.Instance.doesLinkExist(d1, d2)) { - DocUtils.MakeLink({ doc: d1 }, { doc: d2 }); - actionPerformed = true; - } - } - break; - } - if (actionPerformed) { - this._points = []; - } - } - - if (!actionPerformed) { - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { width: B.width, height: B.height, x: B.left, y: B.top }); + @undoBatch + onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { + switch (ge.gesture) { + case GestureUtils.Gestures.Stroke: + const points = ge.points; + const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); + const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height }); this.addDocument(inkDoc); - this._points = []; - } + e.stopPropagation(); + break; + case GestureUtils.Gestures.Box: + const lt = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); + const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); + const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] }; + const bWidth = bounds.r - bounds.x; + const bHeight = bounds.b - bounds.y; + const sel = this.getActiveDocuments().filter(doc => { + const l = NumCast(doc.x); + const r = l + doc[WidthSym](); + const t = NumCast(doc.y); + const b = t + doc[HeightSym](); + const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); + if (pass) { + doc.x = l - bounds.x - bWidth / 2; + doc.y = t - bounds.y - bHeight / 2; + } + return pass; + }); + this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 })); + sel.forEach(d => this.props.removeDocument(d)); + break; + } + } + + @action + onPointerUp = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) return; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -473,13 +447,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } return; } + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + return; + } if (!e.cancelBubble) { const selectedTool = InkingControl.Instance.selectedTool; - if (selectedTool === InkTool.Highlighter || selectedTool === InkTool.Pen || InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - const point = this.getTransform().transformPoint(e.clientX, e.clientY); - this._points.push({ X: point[0], Y: point[1] }); - } - else if (selectedTool === InkTool.None) { + if (selectedTool === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -494,10 +467,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } - handle1PointerMove = (e: TouchEvent) => { + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // panning a workspace if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { if (InkingControl.Instance.selectedTool === InkTool.None) { @@ -510,20 +483,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } this.pan(pt); } - else if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) { - const point = this.getTransform().transformPoint(pt.clientX, pt.clientY); - this._points.push({ X: point[0], Y: point[1] }); - } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } - handle2PointersMove = (e: TouchEvent) => { + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { // pinch zooming if (!e.cancelBubble) { - const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints); + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt1 = myTouches[0]; const pt2 = myTouches[1]; @@ -560,35 +529,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } } } - e.stopPropagation(); + // e.stopPropagation(); e.preventDefault(); } } @action - handle2PointersDown = (e: React.TouchEvent) => { + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { if (!e.nativeEvent.cancelBubble && this.props.active(true)) { - const pt1: React.Touch | null = e.targetTouches.item(0); - const pt2: React.Touch | null = e.targetTouches.item(1); - if (!pt1 || !pt2) return; - - const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; - const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; - this._lastX = centerX; - this._lastY = centerY; - document.removeEventListener("touchmove", this.onTouch); - document.addEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); - document.addEventListener("touchend", this.onTouchEnd); - e.stopPropagation(); + // const pt1: React.Touch | null = e.targetTouches.item(0); + // const pt2: React.Touch | null = e.targetTouches.item(1); + // // if (!pt1 || !pt2) return; + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + if (pt1 && pt2) { + const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2; + const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2; + this._lastX = centerX; + this._lastY = centerY; + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } } } cleanUpInteractions = () => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.removeEventListener("touchmove", this.onTouch); - document.removeEventListener("touchend", this.onTouchEnd); + this.removeMoveListeners(); + this.removeEndListeners(); } @action @@ -885,6 +858,49 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData); } + private thumbIdentifier?: number; + + // @action + // handleHandDown = (e: React.TouchEvent) => { + // const fingers = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true); + // const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]); + // this.thumbIdentifier = thumb?.identifier; + // const others = fingers.filter(f => f !== thumb); + // const minX = Math.min(...others.map(f => f.clientX)); + // const minY = Math.min(...others.map(f => f.clientY)); + // const t = this.getTransform().transformPoint(minX, minY); + // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY); + + // const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc)); + // if (thumbDoc) { + // this._palette = <Palette x={t[0]} y={t[1]} thumb={th} thumbDoc={thumbDoc} />; + // } + + // document.removeEventListener("touchmove", this.onTouch); + // document.removeEventListener("touchmove", this.handleHandMove); + // document.addEventListener("touchmove", this.handleHandMove); + // document.removeEventListener("touchend", this.handleHandUp); + // document.addEventListener("touchend", this.handleHandUp); + // } + + // @action + // handleHandMove = (e: TouchEvent) => { + // for (let i = 0; i < e.changedTouches.length; i++) { + // const pt = e.changedTouches.item(i); + // if (pt?.identifier === this.thumbIdentifier) { + // } + // } + // } + + // @action + // handleHandUp = (e: TouchEvent) => { + // this.onTouchEnd(e); + // if (this.prevPoints.size < 3) { + // this._palette = undefined; + // document.removeEventListener("touchend", this.handleHandUp); + // } + // } + onContextMenu = (e: React.MouseEvent) => { const layoutItems: ContextMenuProps[] = []; @@ -948,34 +964,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ]; } - @computed get svgBounds() { - const xs = this._points.map(p => p.X); - const ys = this._points.map(p => p.Y); - const right = Math.max(...xs); - const left = Math.min(...xs); - const bottom = Math.max(...ys); - const top = Math.min(...ys); - return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top }; - } - - @computed get currentStroke() { - if (this._points.length <= 1) { - return (null); - } - - const B = this.svgBounds; - - return ( - <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, position: "absolute", zIndex: 30000 }}> - {CreatePolyline(this._points, B.left, B.top)} - </svg> - ); - } + // @observable private _palette?: JSX.Element; children = () => { const eles: JSX.Element[] = []; this.extensionDoc && (eles.push(...this.childViews())); - this.currentStroke && (eles.push(this.currentStroke)); + // this._palette && (eles.push(this._palette)); + // this.currentStroke && (eles.push(this.currentStroke)); eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); return eles; } @@ -1010,9 +1005,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (!this.extensionDoc) return (null); // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale; return <div className={"collectionfreeformview-container"} - ref={this.createDropTarget} + ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, - onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} onTouchStart={this.onTouchStart} + onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu} style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, transform: this.contentScaling ? `scale(${this.contentScaling})` : "", diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss new file mode 100644 index 000000000..f57ba438a --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -0,0 +1,33 @@ +.collectionMulticolumnView_contents { + display: flex; + width: 100%; + height: 100%; + overflow: hidden; + + .document-wrapper { + display: flex; + flex-direction: column; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } + + } + + .resizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: column; + + .internal { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 2cb91f239..70e56183c 100644 --- a/src/client/views/collections/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,76 +1,93 @@ import { observer } from 'mobx-react'; -import { makeInterface } from '../../../new_fields/Schema'; -import { documentSchema } from '../../../new_fields/documentSchemas'; -import { CollectionSubView } from './CollectionSubView'; +import { makeInterface } from '../../../../new_fields/Schema'; +import { documentSchema } from '../../../../new_fields/documentSchemas'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import * as React from "react"; -import { Doc } from '../../../new_fields/Doc'; -import { NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; -import { ContentFittingDocumentView } from './../nodes/ContentFittingDocumentView'; -import { Utils } from '../../../Utils'; +import { Doc } from '../../../../new_fields/Doc'; +import { NumCast, StrCast, BoolCast } from '../../../../new_fields/Types'; +import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { Utils } from '../../../../Utils'; import "./collectionMulticolumnView.scss"; -import { computed, trace } from 'mobx'; -import { Transform } from '../../util/Transform'; +import { computed, trace, observable, action } from 'mobx'; +import { Transform } from '../../../util/Transform'; +import WidthLabel from './MulticolumnWidthLabel'; +import ResizeBar from './MulticolumnResizer'; type MulticolumnDocument = makeInterface<[typeof documentSchema]>; const MulticolumnDocument = makeInterface(documentSchema); -interface Unresolved { - target: Doc; +interface WidthSpecifier { magnitude: number; unit: string; } -interface Resolved { - target: Doc; - pixels: number; -} - interface LayoutData { - unresolved: Unresolved[]; - numFixed: number; - numRatio: number; + widthSpecifiers: WidthSpecifier[]; starSum: number; } -const resolvedUnits = ["*", "px"]; -const resizerWidth = 2; -const resizerOpacity = 0.4; +export const WidthUnit = { + Pixel: "px", + Ratio: "*" +}; + +const resolvedUnits = Object.values(WidthUnit); +const resizerWidth = 4; @observer export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) { + /** + * @returns the list of layout documents whose width unit is + * *, denoting that it will be displayed with a ratio, not fixed pixel, value + */ @computed private get ratioDefinedDocs() { - return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === "*"); + return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio); } + /** + * This loops through all childLayoutPairs and extracts the values for widthUnit + * and widthMagnitude, ignoring any that are malformed. Additionally, it then + * normalizes the ratio values so that one * value is always 1, with the remaining + * values proportionate to that easily readable metric. + * @returns the list of the resolved width specifiers (unit and magnitude pairs) + * as well as the sum of the * coefficients, i.e. the ratio magnitudes + */ @computed private get resolvedLayoutInformation(): LayoutData { - const unresolved: Unresolved[] = []; - let starSum = 0, numFixed = 0, numRatio = 0; - - for (const { layout } of this.childLayoutPairs) { - const unit = StrCast(layout.widthUnit); - const magnitude = NumCast(layout.widthMagnitude); + let starSum = 0; + const widthSpecifiers: WidthSpecifier[] = []; + this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => { + const unit = StrCast(widthUnit); + const magnitude = NumCast(widthMagnitude); if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) { - if (unit === "*") { - starSum += magnitude; - numRatio++; - } else { - numFixed++; - } - unresolved.push({ target: layout, magnitude, unit }); + (unit === WidthUnit.Ratio) && (starSum += magnitude); + widthSpecifiers.push({ magnitude, unit }); } - // otherwise, the particular configuration entry is ignored and the remaining - // space is allocated as if the document were absent from the configuration list - } + /** + * Otherwise, the child document is ignored and the remaining + * space is allocated as if the document were absent from the child list + */ + }); + /** + * Here, since these values are all relative, adjustments during resizing or + * manual updating can, though their ratios remain the same, cause the values + * themselves to drift toward zero. Thus, whenever we change any of the values, + * we normalize everything (dividing by the smallest magnitude). + */ setTimeout(() => { - const minimum = Math.min(...this.ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); - this.ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + const { ratioDefinedDocs } = this; + if (this.childLayoutPairs.length) { + const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude))); + if (minimum !== 0) { + ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum); + } + } }); - return { unresolved, numRatio, numFixed, starSum }; + return { widthSpecifiers, starSum }; } /** @@ -83,12 +100,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get totalFixedAllocation(): number | undefined { - return this.resolvedLayoutInformation?.unresolved.reduce( - (sum, { magnitude, unit }) => sum + (unit === "px" ? magnitude : 0), 0); + return this.resolvedLayoutInformation?.widthSpecifiers.reduce( + (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0); } /** - * This returns the total quantity, in pixels, that this + * @returns the total quantity, in pixels, that this * view needs to reserve for child documents that have * (with lower priority) requested a certain relative proportion of the * remaining pixel width not allocated for fixed widths. @@ -98,14 +115,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu */ @computed private get totalRatioAllocation(): number | undefined { - const layoutInfoLen = this.resolvedLayoutInformation?.unresolved.length; + const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length; if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) { return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)); } } /** - * This returns the total quantity, in pixels, that + * @returns the total quantity, in pixels, that * 1* (relative / star unit) is worth. For example, * if the configuration has three documents, with, respectively, * widths of 2*, 2* and 1*, and the panel width returns 1000px, @@ -123,20 +140,37 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu } } + /** + * This wrapper function exists to prevent mobx from + * needlessly rerendering the internal ContentFittingDocumentViews + */ private getColumnUnitLength = () => this.columnUnitLength; + /** + * @param layout the document whose transform we'd like to compute + * Given a layout document, this function + * returns the resolved width it has requested, in pixels. + * @returns the stored column width if already in pixels, + * or the ratio width evaluated to a pixel value + */ private lookupPixels = (layout: Doc): number => { const columnUnitLength = this.columnUnitLength; if (columnUnitLength === undefined) { return 0; // we're still waiting on promises to resolve } let width = NumCast(layout.widthMagnitude); - if (StrCast(layout.widthUnit) === "*") { + if (StrCast(layout.widthUnit) === WidthUnit.Ratio) { width *= columnUnitLength; } return width; } + /** + * @returns the transform that will correctly place + * the document decorations box, shifted to the right by + * the sum of all the resolved column widths of the + * documents before the target. + */ private lookupIndividualTransform = (layout: Doc) => { const columnUnitLength = this.columnUnitLength; if (columnUnitLength === undefined) { @@ -145,27 +179,31 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu let offset = 0; for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - const shift = offset; - return this.props.ScreenToLocalTransform().translate(-shift, 0); + return this.props.ScreenToLocalTransform().translate(-offset, 0); } offset += this.lookupPixels(candidate) + resizerWidth; } return Transform.Identity(); // type coersion, this case should never be hit } + /** + * @returns the resolved list of rendered child documents, displayed + * at their resolved pixel widths, each separated by a resizer. + */ @computed private get contents(): JSX.Element[] | null { - trace(); const { childLayoutPairs } = this; const { Document, PanelHeight } = this.props; const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; collector.push( - <div className={"document-wrapper"}> + <div + className={"document-wrapper"} + key={Utils.GenerateGuid()} + > <ContentFittingDocumentView {...this.props} - key={Utils.GenerateGuid()} Document={layout} DataDocument={layout.resolvedDataDoc as Doc} PanelWidth={() => this.lookupPixels(layout)} @@ -201,99 +239,4 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu ); } -} - -interface SpacerProps { - width: number; - columnUnitLength(): number | undefined; - toLeft?: Doc; - toRight?: Doc; -} - -interface WidthLabelProps { - layout: Doc; - collectionDoc: Doc; - decimals?: number; -} - -@observer -class WidthLabel extends React.Component<WidthLabelProps> { - - @computed - private get contents() { - const { layout, decimals } = this.props; - const magnitude = NumCast(layout.widthMagnitude).toFixed(decimals ?? 3); - const unit = StrCast(layout.widthUnit); - return <span className={"display"}>{magnitude} {unit}</span>; - } - - render() { - return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null); - } - -} - -@observer -class ResizeBar extends React.Component<SpacerProps> { - - private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - window.addEventListener("pointermove", this.onPointerMove); - window.addEventListener("pointerup", this.onPointerUp); - } - - private onPointerMove = ({ movementX }: PointerEvent) => { - const { toLeft, toRight, columnUnitLength } = this.props; - const target = movementX > 0 ? toRight : toLeft; - const unitLength = columnUnitLength(); - if (target && unitLength) { - const { widthUnit, widthMagnitude } = target; - if (widthUnit === "*") { - target.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / unitLength; - } - } - } - - private get isActivated() { - const { toLeft, toRight } = this.props; - if (toLeft && toRight) { - if (StrCast(toLeft.widthUnit) === "px" && StrCast(toRight.widthUnit) === "px") { - return false; - } - return true; - } else if (toLeft) { - if (StrCast(toLeft.widthUnit) === "px") { - return false; - } - return true; - } else if (toRight) { - if (StrCast(toRight.widthUnit) === "px") { - return false; - } - return true; - } - return false; - } - - private onPointerUp = () => { - window.removeEventListener("pointermove", this.onPointerMove); - window.removeEventListener("pointerup", this.onPointerUp); - } - - render() { - return ( - <div - className={"resizer"} - style={{ - width: this.props.width, - opacity: this.isActivated ? resizerOpacity : 0 - }} - onPointerDown={this.registerResizing} - /> - ); - } - }
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx new file mode 100644 index 000000000..11e210958 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { observable, action } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast } from "../../../../new_fields/Types"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface ResizerProps { + width: number; + columnUnitLength(): number | undefined; + toLeft?: Doc; + toRight?: Doc; +} + +enum ResizeMode { + Global = "blue", + Pinned = "red", + Undefined = "black" +} + +const resizerOpacity = 1; + +@observer +export default class ResizeBar extends React.Component<ResizerProps> { + @observable private isHoverActive = false; + @observable private isResizingActive = false; + @observable private resizeMode = ResizeMode.Undefined; + + @action + private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => { + e.stopPropagation(); + e.preventDefault(); + this.resizeMode = mode; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + window.addEventListener("pointermove", this.onPointerMove); + window.addEventListener("pointerup", this.onPointerUp); + this.isResizingActive = true; + } + + private onPointerMove = ({ movementX }: PointerEvent) => { + const { toLeft, toRight, columnUnitLength } = this.props; + const movingRight = movementX > 0; + const toNarrow = movingRight ? toRight : toLeft; + const toWiden = movingRight ? toLeft : toRight; + const unitLength = columnUnitLength(); + if (unitLength) { + if (toNarrow) { + const { widthUnit, widthMagnitude } = toNarrow; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale; + } + if (this.resizeMode === ResizeMode.Pinned && toWiden) { + const { widthUnit, widthMagnitude } = toWiden; + const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1; + toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale; + } + } + } + + private get isActivated() { + const { toLeft, toRight } = this.props; + if (toLeft && toRight) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toLeft) { + if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } else if (toRight) { + if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) { + return false; + } + return true; + } + return false; + } + + @action + private onPointerUp = () => { + this.resizeMode = ResizeMode.Undefined; + this.isResizingActive = false; + this.isHoverActive = false; + window.removeEventListener("pointermove", this.onPointerMove); + window.removeEventListener("pointerup", this.onPointerUp); + } + + render() { + return ( + <div + className={"resizer"} + style={{ + width: this.props.width, + opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0 + }} + onPointerEnter={action(() => this.isHoverActive = true)} + onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))} + > + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)} + style={{ backgroundColor: this.resizeMode }} + /> + <div + className={"internal"} + onPointerDown={e => this.registerResizing(e, ResizeMode.Global)} + style={{ backgroundColor: this.resizeMode }} + /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx new file mode 100644 index 000000000..b394fed62 --- /dev/null +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { Doc } from "../../../../new_fields/Doc"; +import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types"; +import { EditableView } from "../../EditableView"; +import { WidthUnit } from "./CollectionMulticolumnView"; + +interface WidthLabelProps { + layout: Doc; + collectionDoc: Doc; + decimals?: number; +} + +@observer +export default class WidthLabel extends React.Component<WidthLabelProps> { + + @computed + private get contents() { + const { layout, decimals } = this.props; + const getUnit = () => StrCast(layout.widthUnit); + const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3)); + return ( + <div className={"label-wrapper"}> + <EditableView + GetValue={getMagnitude} + SetValue={value => { + const converted = Number(value); + if (!isNaN(converted) && converted > 0) { + layout.widthMagnitude = converted; + return true; + } + return false; + }} + contents={getMagnitude()} + /> + <EditableView + GetValue={getUnit} + SetValue={value => { + if (Object.values(WidthUnit).includes(value)) { + layout.widthUnit = value; + return true; + } + return false; + }} + contents={getUnit()} + /> + </div> + ); + } + + render() { + return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null); + } + +}
\ No newline at end of file |
