diff options
Diffstat (limited to 'src/client/views/nodes/DocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 894 |
1 files changed, 408 insertions, 486 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7cbd96354..7dbefe087 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,17 +1,14 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx"; +import { action, computed, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import * as rp from "request-promise"; -import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; -import { Copy, Id } from '../../../new_fields/FieldSymbols'; -import { List } from "../../../new_fields/List"; -import { ObjectField } from "../../../new_fields/ObjectField"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; +import { Id } from '../../../new_fields/FieldSymbols'; import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; import { ScriptField } from '../../../new_fields/ScriptField'; -import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { RouteStore } from '../../../server/RouteStore'; import { emptyFunction, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; @@ -35,14 +32,17 @@ import { MainView } from '../MainView'; import { OverlayView } from '../OverlayView'; import { ScriptBox } from '../ScriptBox'; import { ScriptingRepl } from '../ScriptingRepl'; -import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; -const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../../new_fields/URLField'; +import SharingManager from '../../util/SharingManager'; +import { Scripting } from '../../util/Scripting'; +library.add(fa.faEdit); library.add(fa.faTrash); library.add(fa.faShare); library.add(fa.faDownload); @@ -65,19 +65,9 @@ library.add(fa.faUnlock); library.add(fa.faLock); library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); -// const linkSchema = createSchema({ -// title: "string", -// linkDescription: "string", -// linkTags: "string", -// linkedTo: Doc, -// linkedFrom: Doc -// }); - -// type LinkDoc = makeInterface<[typeof linkSchema]>; -// const LinkDoc = makeInterface(linkSchema); - export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + ContainingCollectionDoc: Opt<Doc>; Document: Doc; DataDoc?: Doc; fitToBox?: boolean; @@ -89,48 +79,52 @@ export interface DocumentViewProps { renderDepth: number; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; ContentScaling: () => number; + ruleProvider: Doc | undefined; PanelWidth: () => number; PanelHeight: () => number; - focus: (doc: Doc, willZoom: boolean, scale?: number) => void; - selectOnLoad: boolean; + focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void; + addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean; pinToPres: (document: Doc) => void; - collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void; zoomToScale: (scale: number) => void; backgroundColor: (doc: Doc) => string | undefined; getScale: () => number; - animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void; + animateBetweenIcon?: (maximize: boolean, target: number[]) => void; ChromeHeight?: () => number; } -const schema = createSchema({ - layout: "string", - nativeWidth: "number", - nativeHeight: "number", - backgroundColor: "string", - opacity: "number", - hidden: "boolean", - onClick: ScriptField, -}); - -export const positionSchema = createSchema({ - nativeWidth: "number", - nativeHeight: "number", - width: "number", - height: "number", - x: "number", - y: "number", - z: "number", +export const documentSchema = createSchema({ + // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out + title: "string", // document title (can be on either data document or layout) + nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set + nativeHeight: "number", // " + width: "number", // width of document in its container's coordinate system + height: "number", // " + backgroundColor: "string", // background color of document + opacity: "number", // opacity of document + onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop) + ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document + autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents + isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed + isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee) + type: "string", // enumerated type of document + maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) + lockedPosition: "boolean", // whether the document can be spatially manipulated + inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently + borderRounding: "string", // border radius rounding of document + searchFields: "string", // the search fields to display when this document matches a search in its metadata + heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc) + showCaption: "string", // whether editable caption text is overlayed at the bottom of the document + showTitle: "string", // whether an editable title banner is displayed at tht top of the document + isButton: "boolean", // whether document functions as a button (overiding native interactions of its content) + ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events) }); -export type PositionDocument = makeInterface<[typeof positionSchema]>; -export const PositionDocument = makeInterface(positionSchema); -type Document = makeInterface<[typeof schema]>; -const Document = makeInterface(schema); +type Document = makeInterface<[typeof documentSchema]>; +const Document = makeInterface(documentSchema); @observer export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) { @@ -138,115 +132,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _downY: number = 0; private _lastTap: number = 0; private _doubleTap = false; - private _hitExpander = false; private _hitTemplateDrag = false; private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; - _animateToIconDisposer?: IReactionDisposer; - _reactionDisposer?: IReactionDisposer; 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.renderDepth === 0; } - @computed get templates(): List<string> { - let field = this.props.Document.templates; - if (field && field instanceof List) { - return field; - } - return new List<string>(); - } - set templates(templates: List<string>) { this.props.Document.templates = templates; } - screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); } + @computed get topMost() { return this.props.renderDepth === 0; } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } @action componentDidMount() { - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } - // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes - this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title), - this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], - () => { - let maxDoc = DocListCast(this.props.Document.maximizedDocs); - if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { - this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; - } - let sumDoc = Cast(this.props.Document.summaryDoc, Doc); - if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { - this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; - } - }, { fireImmediately: true }); - this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => - (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false) - , { fireImmediately: true }); + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); DocumentManager.Instance.DocumentViews.push(this); } - animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => { - this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) : - DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing); - } - - public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => { - setTimeout(() => { - let now = Date.now(); - let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1; - doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - cb && cb(progress); - if (now < stime + 200) { - DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb); - } - else { - doc.isMinimized = !maximizing; - doc.isIconAnimating = undefined; - } - doc.willMaximize = false; - }, - 2); - } @action componentDidUpdate() { this._dropDisposer && this._dropDisposer(); - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { - handlers: { drop: this.drop.bind(this) } - }); - } + this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } })); } + @action componentWillUnmount() { - this._reactionDisposer && this._reactionDisposer(); - this._animateToIconDisposer && this._animateToIconDisposer(); this._dropDisposer && this._dropDisposer(); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } - stopPropagation = (e: React.SyntheticEvent) => { - e.stopPropagation(); - } - - get dataDoc() { - if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) { - // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document - // has a template layout document, then we will render the template layout but use - // this document as the data document for the layout. - return this.props.Document; - } - return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined; - } - startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) { + startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) { if (this._mainCont.current) { - let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; - let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])]; + let dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); - let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected); - const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.dropAction = dropAction; - dragData.xOffset = xoff; - dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; dragData.applyAsTemplate = applyAsTemplate; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { @@ -257,163 +177,92 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } } - toggleMinimized = async () => { - let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (minimizedDoc) { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint( - NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y)); - this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs)); - } - } - static _undoBatch?: UndoManager.Batch = undefined; - @action - public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => { - SelectionManager.DeselectAll(); - if (expandedDocs) { - if (!DocumentView._undoBatch) { - DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); - } - let isMinimized: boolean | undefined; - expandedDocs.map(maximizedDoc => { - let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { - if (isMinimized === undefined) { - isMinimized = BoolCast(maximizedDoc.isMinimized); - } - maximizedDoc.willMaximize = isMinimized; - maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); + onClick = async (e: React.MouseEvent) => { + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + e.stopPropagation(); + let preventDefault = true; + if (this._doubleTap && this.props.renderDepth) { + let fullScreenAlias = Doc.MakeAlias(this.props.Document); + let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc)); + if (layoutNative && fullScreenAlias.layout === layoutNative.layout) { + await swapViews(fullScreenAlias, "layoutCustom", "layoutNative"); } - }); - setTimeout(() => { - DocumentView._undoBatch && DocumentView._undoBatch.end(); - DocumentView._undoBatch = undefined; - }, 500); + this.props.addDocTab(fullScreenAlias, undefined, "inTab"); + SelectionManager.DeselectAll(); + Doc.UnBrushDoc(this.props.Document); + } else if (this.onClickHandler && this.onClickHandler.script) { + this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log); + } else if (this.Document.isButton) { + SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered. + this.buttonClick(e.altKey, e.ctrlKey); + } else { + SelectionManager.SelectDoc(this, e.ctrlKey); + preventDefault = false; + } + preventDefault && e.preventDefault(); } } - onClick = async (e: React.MouseEvent) => { - if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing. - if (this.onClickHandler && this.onClickHandler.script) { - e.stopPropagation(); - this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }); - e.preventDefault(); - return; - } - let altKey = e.altKey; - let ctrlKey = e.ctrlKey; - if (this._doubleTap && this.props.renderDepth) { - e.stopPropagation(); - let fullScreenAlias = Doc.MakeAlias(this.props.Document); - fullScreenAlias.templates = new List<string>(); - Doc.UseDetailLayout(fullScreenAlias); - fullScreenAlias.showCaption = true; - this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); + buttonClick = async (altKey: boolean, ctrlKey: boolean) => { + let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); + let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); + let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); + let expandedDocs: Doc[] = []; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); - } - else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && - (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { - if (BoolCast(this.props.Document.ignoreClick)) { - return; - } - e.stopPropagation(); - SelectionManager.SelectDoc(this, e.ctrlKey); - let isExpander = (e.target as any).id === "isExpander"; - if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) { - let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); - let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); - let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document); - let expandedDocs: Doc[] = []; - expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; - expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; - expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; - // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; - if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents - SelectionManager.DeselectAll(); - let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); - let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); - if (altKey || ctrlKey) { - maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace")); - if (!maxLocation || maxLocation === "inPlace") { - let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false); - expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); - let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); - if (!hasView) { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false)); - } - expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); - } - } - if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) { - let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); - if (dataDocs) { - expandedDocs.forEach(maxDoc => - (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && - this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation))); - } - } else { - let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); - this.collapseTargetsToPoint(scrpt, expandedDocs); - } - } - else if (linkedDocs.length) { - SelectionManager.DeselectAll(); - let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document)); - let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]]; - - // @TODO: shouldn't always follow target context - let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined]; - - let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined]; - - if (!linkedFwdDocs.some(l => l instanceof Promise)) { - let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab"); - let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined; - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false, - document => { // open up target if it's not already in view ... - this.props.focus(this.props.Document, true, 1); // by zooming into the button document first - setTimeout(() => this.props.addDocTab(document, undefined, maxLocation), 1000); // then after the 1sec animation, open up the target in a new tab - }, - linkedFwdPage[altKey ? 1 : 0], targetContext); - } - } + let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace"); + maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab")); + if (maxLocation === "inPlace") { + expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc, false)); + let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2); + DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs); + } else { + expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation))); } } + else if (linkDocs.length) { + DocumentManager.Instance.FollowLink(undefined, this.props.Document, + // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards + (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)), + ctrlKey, altKey, this.props.ContainingCollectionDoc); + } } - onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble) return; + if (e.nativeEvent.cancelBubble && e.button === 0) return; this._downX = e.clientX; this._downY = e.clientY; - this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; this._hitTemplateDrag = false; + // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where + // this document is the template and we apply it to whatever we drop it on. for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) { if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") { this._hitTemplateDrag = true; } } - if (this.active) e.stopPropagation(); // events stop at the lowest document that is active. + if (this.active && e.button === 0 && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); } } onPointerMove = (e: PointerEvent): void => { + if ((e as any).formattedHandled) { e.stopPropagation(); return; } if (e.cancelBubble && this.active) { - document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView) } - else if (!e.cancelBubble && this.active) { - if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) { - if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) { + else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition && !this.Document.inOverlay) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + if (!e.altKey && !this.topMost && e.buttons === 1) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag); + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag); } } 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 @@ -431,71 +280,79 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } @undoBatch - fieldsClicked = (): void => { - let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); - this.props.addDocTab(kvp, this.dataDoc, "onRight"); - } - - @undoBatch - makeBtnClicked = (): void => { - let doc = Doc.GetProto(this.props.Document); - doc.isButton = !BoolCast(doc.isButton); - if (doc.isButton) { - if (!doc.nativeWidth) { - doc.nativeWidth = this.props.Document[WidthSym](); - doc.nativeHeight = this.props.Document[HeightSym](); + static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom") + + static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => { + const batch = UndoManager.StartBatch("CustomViewClicked"); + if (doc.layoutCustom === undefined) { + Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative"); + await swapViews(doc, "", "layoutNative"); + + const width = NumCast(doc.width); + const height = NumCast(doc.height); + const options = { title: "data", width, x: -width / 2, y: - height / 2, }; + + let fieldTemplate: Doc; + switch (doc.type) { + case DocumentType.TEXT: + fieldTemplate = Docs.Create.TextDocument(options); + break; + case DocumentType.PDF: + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + break; + case DocumentType.VID: + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + break; + default: + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); } + + fieldTemplate.backgroundColor = doc.backgroundColor; + fieldTemplate.heading = 1; + fieldTemplate.autoHeight = true; + + let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) }); + + Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true); + Doc.ApplyTemplateTo(docTemplate, doc, undefined); + Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom"); } else { - doc.nativeWidth = doc.nativeHeight = undefined; + await swapViews(doc, "layoutCustom", "layoutNative"); } + batch.end(); } @undoBatch - public fullScreenClicked = (): void => { - CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this); - SelectionManager.DeselectAll(); + makeBtnClicked = (): void => { + if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) { + this.Document.isButton = false; + this.Document.ignoreClick = false; + this.Document.onClick = undefined; + } else { + this.Document.isButton = true; + } } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.AnnotationDragData) { + /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - let annotationDoc = de.data.annotationDocument; - annotationDoc.linkedToDoc = true; - de.data.targetContext = this.props.ContainingCollectionView!.props.Document; - let targetDoc = this.props.Document; - targetDoc.targetContext = de.data.targetContext; - let annotations = await DocListCastAsync(annotationDoc.annotations); - annotations && annotations.forEach(anno => anno.target = targetDoc); - - DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); + (de.data as any).linkedToDoc = true; + + DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`); } if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) { - Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc); + Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document); e.stopPropagation(); } if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc = de.data.linkSourceDocument; - let destDoc = this.props.Document; - e.stopPropagation(); - if (de.mods === "AltKey") { - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - let src = protoSrc ? protoSrc : sourceDoc; - let dst = protoDest ? protoDest : destDoc; - dst.data = (src.data! as ObjectField)[Copy](); - dst.nativeWidth = src.nativeWidth; - dst.nativeHeight = src.nativeHeight; - } - else { - // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); - // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); - let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); - de.data.droppedDocuments.push(destDoc); - de.data.linkDocument = linkDoc; - } + // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); + // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); + de.data.linkSourceDocument !== this.props.Document && + (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed } } @@ -503,58 +360,61 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { - let oldLayout = FieldValue(this.Document.layout) || ""; + let oldLayout = StrCast(this.props.Document.layout); let layout = text.replace("{layout}", oldLayout); - this.Document.layout = layout; + this.props.Document.layout = layout; e.stopPropagation(); e.preventDefault(); } } + @undoBatch @action - addTemplate = (template: Template) => { - this.templates.push(template.Layout); - this.templates = this.templates; + freezeNativeDimensions = (): void => { + let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); + proto.autoHeight = this.Document.autoHeight = false; + proto.ignoreAspect = !proto.ignoreAspect; + if (!proto.ignoreAspect && !proto.nativeWidth) { + proto.nativeWidth = this.props.PanelWidth(); + proto.nativeHeight = this.props.PanelHeight(); + } } + @undoBatch @action - removeTemplate = (template: Template) => { - for (let i = 0; i < this.templates.length; i++) { - if (this.templates[i] === template.Layout) { - this.templates.splice(i, 1); - break; - } + makeIntoPortal = async () => { + let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc))); + if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) { + let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + DocServer.GetRefField(portalID).then(existingPortal => { + let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID }); + DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link"); + this.Document.isButton = true; + }); } - this.templates = this.templates; - } - @action - clearTemplates = () => { - this.templates.length = 0; - this.templates = this.templates; } @undoBatch @action - freezeNativeDimensions = (): void => { - let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document); - this.props.Document.autoHeight = proto.autoHeight = false; - proto.ignoreAspect = !BoolCast(proto.ignoreAspect); - if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) { - proto.nativeWidth = this.props.PanelWidth(); - proto.nativeHeight = this.props.PanelHeight(); + setCustomView = (custom: boolean): void => { + if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) { + Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc); + } else { // bcz: not robust -- for now documents with string layout are native documents, and those with Doc layouts are customized + custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document); } } + @undoBatch @action makeBackground = (): void => { - this.props.Document.isBackground = !this.props.Document.isBackground; - this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true); + this.Document.isBackground = !this.Document.isBackground; + this.Document.isBackground && this.props.bringToFront(this.Document, true); } @undoBatch @action toggleLockPosition = (): void => { - this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true; + this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true; } listen = async () => { @@ -582,45 +442,60 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const cm = ContextMenu.Instance; let subitems: ContextMenuProps[] = []; - subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" }); - subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" }); - subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" }); - subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); + subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" }); + subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" }); + subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" }); + subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - let existingMake = ContextMenu.Instance.findByDescription("Make..."); - let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; - makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); - makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" }); - makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); - makes.push({ - description: "Into Portal", event: () => { - let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); - DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal"); - this.makeBtnClicked(); - }, icon: "window-restore" + + if (Cast(this.props.Document.data, ImageField)) { + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + + let existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" }); + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" }); + onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) }); + onClicks.push({ + description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => { + this.props.Document.collectionContext = this.props.ContainingCollectionDoc; + ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n"); + } }); - makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" }); - !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); + !existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + let existing = ContextMenu.Instance.findByDescription("Layout..."); let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; - - layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); - layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" }); - layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); - layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" }); + if (this.props.DataDoc) { + layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" }); + } + layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.Document.autoHeight = !this.Document.autoHeight, icon: "plus" }); + layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); + layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" }); - if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { - layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); + if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) { + layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + } else if (this.props.Document.layoutNative) { + layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" }); } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); if (!ClientUtils.RELEASE) { - let copies: ContextMenuProps[] = []; - copies.push({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" }); - copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); - cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); + // let copies: ContextMenuProps[] = []; + cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); + // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; @@ -629,44 +504,20 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); cm.addItem({ - description: "Download document", icon: "download", event: () => { - const a = document.createElement("a"); - const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); - a.href = url; - a.download = `DocExport-${this.props.Document[Id]}.zip`; - a.click(); - } + description: "Download document", icon: "download", event: async () => + console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), { + qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' } + }))) + // const a = document.createElement("a"); + // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`); + // a.href = url; + // a.download = `DocExport-${this.props.Document[Id]}.zip`; + // a.click(); }); + cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); - type User = { email: string, userDocumentId: string }; - let usersMenu: ContextMenuProps[] = []; - try { - let stuff = await rp.get(Utils.prepend(RouteStore.getUsers)); - const users: User[] = JSON.parse(stuff); - usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({ - description: email, event: async () => { - const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc); - if (!userDocument) { - throw new Error(`Couldn't get user document of user ${email}`); - } - const notifDoc = await Cast(userDocument.optionalRightCollection, Doc); - if (notifDoc instanceof Doc) { - const data = await Cast(notifDoc.data, listSpec(Doc)); - const sharedDoc = Doc.MakeAlias(this.props.Document); - if (data) { - data.push(sharedDoc); - } else { - notifDoc.data = new List([sharedDoc]); - } - } - }, icon: "male" - })); - } catch { - - } runInAction(() => { - cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" }); if (!ClientUtils.RELEASE) { let setWriteMode = (mode: DocServer.WriteMode) => { DocServer.AclsMode = mode; @@ -690,6 +541,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" }); cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" }); } + }); + runInAction(() => { + cm.addItem({ + description: "Share", + event: () => SharingManager.Instance.open(this), + icon: "external-link-alt" + }); if (!this.topMost) { // DocumentViews should stop propagation of this event @@ -702,117 +560,181 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; - onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; + + // the document containing the view layout information - will be the Document itself unless the Document has + // a layout field. In that case, all layout information comes from there unless overriden by Document + get layoutDoc(): Document { + return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document); + } + + // does Document set a layout prop + setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)]; + // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise. + getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); + getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]); isSelected = () => SelectionManager.IsSelected(this); - @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; - @computed get nativeWidth() { return this.Document.nativeWidth || 0; } - @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; } + select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }; + + chromeHeight = () => { + let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle); + return (showTitle ? 25 : 0) + 1; + } + + childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); @computed get contents() { - return (<DocumentContentsView {...this.props} + return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + Document={this.props.Document} + fitToBox={this.props.fitToBox} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + renderDepth={this.props.renderDepth} + showOverlays={this.props.showOverlays} + ContentScaling={this.childScaling} + ruleProvider={this.props.ruleProvider} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + focus={this.props.focus} + parentActive={this.props.parentActive} + whenActiveChanged={this.props.whenActiveChanged} + bringToFront={this.props.bringToFront} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + zoomToScale={this.props.zoomToScale} + backgroundColor={this.props.backgroundColor} + animateBetweenIcon={this.props.animateBetweenIcon} + getScale={this.props.getScale} ChromeHeight={this.chromeHeight} isSelected={this.isSelected} select={this.select} onClick={this.onClickHandler} - selectOnLoad={this.props.selectOnLoad} - layoutKey={"layout"} - fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox} - DataDoc={this.dataDoc} />); + layoutKey="layout" + DataDoc={this.props.DataDoc} />); } - - chromeHeight = () => { - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - }); - } - return (showTitle ? 25 : 0) + 1;// bcz: why 8?? - } - - get layoutDoc() { - // if this document's layout field contains a document (ie, a rendering template), then we will use that - // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. - return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document; - } - - render() { - let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ? - this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) : - StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc); - let foregroundColor = StrCast(this.layoutDoc.color); - var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%"; - var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; - let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; - let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); - let templates = Cast(this.layoutDoc.templates, listSpec("string")); - if (!showOverlays && templates instanceof List) { - templates.map(str => { - if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - if (!showCaption && str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption"; - }); - } - let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined; - let brushDegree = Doc.IsBrushedDegree(this.props.Document); - let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding); - let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree; + let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined; + const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined; + const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined; + const colorSet = this.setsLayoutProp("backgroundColor"); + const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground; + const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ? + this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) : + ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document); + + const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%"; + const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%"; + const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined; + const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle"); + const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption"); + const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined; + const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); + const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding; + const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree; + const searchHighlight = (!this.Document.searchFields ? (null) : + <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + {this.Document.searchFields} + </div>); + const captionView = (!showCaption ? (null) : + <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> + <FormattedTextBox {...this.props} + onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue} + isSelected={this.isSelected} focus={emptyFunction} select={this.select} + fieldExt={""} hideOnLeave={true} fieldKey={showCaption} + /> + </div>); + const titleView = (!showTitle ? (null) : + <div className="documentView-titleWrapper" style={{ + position: showTextTitle ? "relative" : "absolute", + pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", + width: `${100 * this.props.ContentScaling()}%`, + transform: `scale(${1 / this.props.ContentScaling()})` + }}> + <EditableView + contents={this.Document[showTitle]} + display={"block"} height={72} fontSize={12} + GetValue={() => StrCast(this.Document[showTitle])} + SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true} + /> + </div>); + let animheight = animDims ? animDims[1] : nativeHeight; + let animwidth = animDims ? animDims[0] : nativeWidth; + + const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; + const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"]; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ - pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", - color: foregroundColor, - outlineColor: ["transparent", "maroon", "maroon"][brushDegree], - outlineStyle: ["none", "dashed", "solid"][brushDegree], - outlineWidth: brushDegree && !borderRounding ? `${localScale}px` : "0px", - border: brushDegree && borderRounding ? `${["none", "dashed", "solid"][brushDegree]} ${["transparent", "maroon", "maroon"][brushDegree]} ${localScale}px` : undefined, - borderRadius: "inherit", + transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition), + pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all", + color: StrCast(this.Document.color), + outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px", + border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined, background: backgroundColor, - width: nativeWidth, - height: nativeHeight, - transform: `scale(${this.props.ContentScaling()})`, + width: animwidth, + height: animheight, + transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`, opacity: this.Document.opacity }} onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} + onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)} > - {!showTitle && !showCaption ? this.contents : - <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}> - - <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}> + {!showTitle && !showCaption ? + this.Document.searchFields ? + (<div className="documentView-searchWrapper"> + {this.contents} + {searchHighlight} + </div>) + : + this.contents + : + <div className="documentView-styleWrapper" > + <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}> {this.contents} </div> - {!showTitle ? (null) : - <div style={{ - position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre", - pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all", - overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white", - transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})` - }}> - <EditableView - contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]} - display={"block"} - height={72} - fontSize={12} - GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])} - SetValue={(value: string) => ((this.layoutDoc.isTemplate ? this.layoutDoc : Doc.GetProto(this.layoutDoc))[showTitle!] = value) ? true : true} - /> - </div> - } - {!showCaption ? (null) : - <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}> - <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> - </div> - } + {titleView} + {captionView} + {searchHighlight} </div> } </div> ); } -}
\ No newline at end of file +} + +export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) { + let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc); + if (oldLayoutExt) { + oldLayoutExt.autoHeight = doc.autoHeight; + oldLayoutExt.width = doc.width; + oldLayoutExt.height = doc.height; + oldLayoutExt.nativeWidth = doc.nativeWidth; + oldLayoutExt.nativeHeight = doc.nativeHeight; + oldLayoutExt.ignoreAspect = doc.ignoreAspect; + oldLayoutExt.backgroundLayout = doc.backgroundLayout; + oldLayoutExt.type = doc.type; + oldLayoutExt.layout = doc.layout; + } + + let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc); + if (newLayoutExt) { + doc.autoHeight = newLayoutExt.autoHeight; + doc.width = newLayoutExt.width; + doc.height = newLayoutExt.height; + doc.nativeWidth = newLayoutExt.nativeWidth; + doc.nativeHeight = newLayoutExt.nativeHeight; + doc.ignoreAspect = newLayoutExt.ignoreAspect; + doc.backgroundLayout = newLayoutExt.backgroundLayout; + doc.type = newLayoutExt.type; + doc.layout = await newLayoutExt.layout; + } +} + +Scripting.addGlobal(function toggleDetail(doc: any) { + let native = typeof doc.layout === "string"; + swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom"); +});
\ No newline at end of file |