diff options
| author | bob <bcz@cs.brown.edu> | 2019-08-19 10:11:59 -0400 |
|---|---|---|
| committer | bob <bcz@cs.brown.edu> | 2019-08-19 10:11:59 -0400 |
| commit | e37bf9124c952aa26c3e29deb9e4faa01cad1a7e (patch) | |
| tree | be44ae9bd5e2eb6c5ce392383d41505b5863d061 /src/client/views/nodes | |
| parent | 07482c3bf435748140addfd4fd338fc668657798 (diff) | |
| parent | b037aa89fb564812f880994453ce002054a0ad82 (diff) | |
Merge branch 'master' into presentation_f
Diffstat (limited to 'src/client/views/nodes')
23 files changed, 921 insertions, 545 deletions
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss index 92beafa15..5ed435505 100644 --- a/src/client/views/nodes/ButtonBox.scss +++ b/src/client/views/nodes/ButtonBox.scss @@ -3,10 +3,14 @@ height: 100%; pointer-events: all; border-radius: inherit; + display:table; } .buttonBox-mainButton { width: 100%; height: 100%; border-radius: inherit; + display:table-cell; + vertical-align: middle; + text-align: center; }
\ No newline at end of file diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx index d2c23fdab..8b6f11aac 100644 --- a/src/client/views/nodes/ButtonBox.tsx +++ b/src/client/views/nodes/ButtonBox.tsx @@ -15,8 +15,9 @@ import { Doc } from '../../../new_fields/Doc'; import './ButtonBox.scss'; import { observer } from 'mobx-react'; import { DocumentIconContainer } from './DocumentIcon'; +import { StrCast } from '../../../new_fields/Types'; -library.add(faEdit); +library.add(faEdit as any); const ButtonSchema = createSchema({ onClick: ScriptField, @@ -30,47 +31,10 @@ const ButtonDocument = makeInterface(ButtonSchema); export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) { public static LayoutString() { return FieldView.LayoutString(ButtonBox); } - onClick = (e: React.MouseEvent) => { - const onClick = this.Document.onClick; - if (!onClick) { - return; - } - e.stopPropagation(); - e.preventDefault(); - onClick.script.run({ this: this.props.Document }); - } - - onContextMenu = () => { - ContextMenu.Instance.addItem({ - description: "Edit OnClick script", icon: "edit", event: () => { - let overlayDisposer: () => void = emptyFunction; - const script = this.Document.onClick; - let originalText: string | undefined = undefined; - if (script) originalText = script.script.originalScript; - // tslint:disable-next-line: no-unnecessary-callback-wrapper - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { - const script = CompileScript(text, { - params: { this: Doc.name }, - typecheck: false, - editable: true, - transformer: DocumentIconContainer.getTransformer() - }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - return; - } - this.Document.onClick = new ScriptField(script); - overlayDisposer(); - }} showDocumentIcons />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnClick` }); - } - }); - } - render() { return ( - <div className="buttonBox-outerDiv" onContextMenu={this.onContextMenu}> - <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || "Button"}</button> + <div className="buttonBox-outerDiv" > + <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >{this.Document.text || this.Document.title}</div> </div> ); } diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7ffd760e0..ee596c841 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -8,6 +8,7 @@ import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView" import "./DocumentView.scss"; import React = require("react"); import { Doc } from "../../../new_fields/Doc"; +import { returnEmptyString } from "../../../Utils"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { x?: number; @@ -69,6 +70,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF return undefined; } + @computed + get clusterColor() { return this.props.backgroundColor(this.props.Document); } + + clusterColorFunc = (doc: Doc) => this.clusterColor; + render() { const hasPosition = this.props.x !== undefined || this.props.y !== undefined; return ( @@ -77,6 +83,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transformOrigin: "left top", position: "absolute", backgroundColor: "transparent", + boxShadow: this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` : + this.clusterColor ? ( + this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` : + `${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined, borderRadius: this.borderRounding(), transform: this.transform, transition: hasPosition ? "transform 1s" : StrCast(this.props.Document.transition), @@ -87,6 +97,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF <DocumentView {...this.props} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} + backgroundColor={this.clusterColorFunc} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} animateBetweenIcon={this.animateBetweenIcon} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index f74336cdd..b901bdcfb 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,6 +11,7 @@ import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; +import { DragBox } from "./DragBox"; import { ButtonBox } from "./ButtonBox"; import { PresBox } from "./PresBox"; import { IconBox } from "./IconBox"; @@ -19,6 +20,7 @@ import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { FieldView } from "./FieldView"; import { WebBox } from "./WebBox"; +import { YoutubeBox } from "./../../apis/youtube/YoutubeBox"; import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; @@ -27,7 +29,7 @@ import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { Doc } from "../../../new_fields/Doc"; import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox"; -import { CollectionViewType } from "../collections/CollectionBaseView"; +import { ScriptField } from "../../../new_fields/ScriptField"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -48,6 +50,7 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; export class DocumentContentsView extends React.Component<DocumentViewProps & { isSelected: () => boolean, select: (ctrl: boolean) => void, + onClick?: ScriptField, layoutKey: string, hideOnLeave?: boolean }> { @@ -80,7 +83,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { } CreateBindings(): JsxBindings { - return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc } }; + let list = { + ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, + Document: this.layoutDoc, + DataDoc: this.dataDoc + }; + return { props: list }; } @computed get templates(): List<string> { @@ -99,10 +107,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { if (this.props.renderDepth > 7) return (null); if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null); return <ObserverJsxParser - components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox }} + blacklistedAttrs={[]} + components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }} bindings={this.CreateBindings()} jsx={this.finalLayout} showWarnings={true} + onError={(test: any) => { console.log(test); }} />; } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 228efdc87..08d17e559 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,19 +1,25 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import * as fa from '@fortawesome/free-solid-svg-icons'; -import { action, computed, IReactionDisposer, reaction, trace, observable, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc"; +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 { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { BoolCast, Cast, FieldValue, StrCast, NumCast, PromiseValue } from "../../../new_fields/Types"; +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 { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { emptyFunction, Utils, returnFalse, returnTrue } from "../../../Utils"; +import { RouteStore } from '../../../server/RouteStore'; +import { emptyFunction, returnTrue, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; -import { Docs, DocUtils, DocumentType } from "../../documents/Documents"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { ClientUtils } from '../../util/ClientUtils'; +import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; -import { SearchUtil } from "../../util/SearchUtil"; +import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; @@ -22,30 +28,25 @@ import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; +import { EditableView } from '../EditableView'; +import { MainView } from '../MainView'; +import { OverlayView } from '../OverlayView'; import { PresentationView } from "../presentationview/PresentationView"; -import { Template, Templates } from "./../Templates"; +import { ScriptBox } from '../ScriptBox'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; -import * as rp from "request-promise"; import "./DocumentView.scss"; -import React = require("react"); -import { Id, Copy } from '../../../new_fields/FieldSymbols'; -import { ContextMenuProps } from '../ContextMenuItem'; -import { list, object, createSimpleSchema } from 'serializr'; -import { LinkManager } from '../../util/LinkManager'; -import { RouteStore } from '../../../server/RouteStore'; import { FormattedTextBox } from './FormattedTextBox'; -import { OverlayView } from '../OverlayView'; -import { ScriptingRepl } from '../ScriptingRepl'; -import { ClientUtils } from '../../util/ClientUtils'; -import { EditableView } from '../EditableView'; -import { faHandPointer, faHandPointRight } from '@fortawesome/free-regular-svg-icons'; -import { DocumentDecorations } from '../DocumentDecorations'; +import React = require("react"); import { PresBox } from './PresBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); library.add(fa.faShare); +library.add(fa.faDownload); library.add(fa.faExpandArrowsAlt); library.add(fa.faCompressArrowsAlt); library.add(fa.faLayerGroup); @@ -63,7 +64,7 @@ library.add(fa.faCrosshairs); library.add(fa.faDesktop); library.add(fa.faUnlock); library.add(fa.faLock); -library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake); +library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone); // const linkSchema = createSchema({ // title: "string", @@ -81,6 +82,7 @@ export interface DocumentViewProps { Document: Doc; DataDoc?: Doc; fitToBox?: boolean; + onClick?: ScriptField; addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean; removeDocument?: (doc: Doc) => boolean; moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; @@ -94,12 +96,14 @@ export interface DocumentViewProps { selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; - bringToFront: (doc: Doc) => void; + bringToFront: (doc: Doc, sendToBack?: boolean) => void; addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => 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; + ChromeHeight?: () => number; } const schema = createSchema({ @@ -108,7 +112,8 @@ const schema = createSchema({ nativeHeight: "number", backgroundColor: "string", opacity: "number", - hidden: "boolean" + hidden: "boolean", + onClick: ScriptField, }); export const positionSchema = createSchema({ @@ -118,6 +123,7 @@ export const positionSchema = createSchema({ height: "number", x: "number", y: "number", + z: "number", }); export type PositionDocument = makeInterface<[typeof positionSchema]>; @@ -135,6 +141,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _hitExpander = 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(); } @@ -149,12 +157,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu set templates(templates: List<string>) { this.props.Document.templates = templates; } screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); - constructor(props: DocumentViewProps) { - super(props); - } - - _animateToIconDisposer?: IReactionDisposer; - _reactionDisposer?: IReactionDisposer; @action componentDidMount() { if (this._mainCont.current) { @@ -206,9 +208,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action componentDidUpdate() { - if (this._dropDisposer) { - this._dropDisposer(); - } + this._dropDisposer && this._dropDisposer(); if (this._mainCont.current) { this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } @@ -217,9 +217,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action componentWillUnmount() { - if (this._reactionDisposer) this._reactionDisposer(); - if (this._animateToIconDisposer) this._animateToIconDisposer(); - if (this._dropDisposer) this._dropDisposer(); + this._reactionDisposer && this._reactionDisposer(); + this._animateToIconDisposer && this._animateToIconDisposer(); + this._dropDisposer && this._dropDisposer(); DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); } @@ -294,6 +294,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClick = async (e: React.MouseEvent) => { if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing. e.stopPropagation(); + if (this.onClickHandler && this.onClickHandler.script) { + 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) { @@ -303,7 +308,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu fullScreenAlias.showCaption = true; this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab"); SelectionManager.DeselectAll(); - this.props.Document.libraryBrush = false; + Doc.UnBrushDoc(this.props.Document); } else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && @@ -361,18 +366,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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 => { - this.props.focus(this.props.Document, true, 1); - setTimeout(() => - this.props.addDocTab(document, undefined, maxLocation), 1000); - } - , linkedFwdPage[altKey ? 1 : 0], targetContext); + 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); } } } } } onPointerDown = (e: React.PointerEvent): void => { + if (e.nativeEvent.cancelBubble) return; this._downX = e.clientX; this._downY = e.clientY; this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; @@ -411,7 +417,10 @@ 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"); } + fieldsClicked = (): void => { + let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); + this.props.addDocTab(kvp, this.dataDoc, "onRight"); + } @undoBatch makeBtnClicked = (): void => { @@ -440,17 +449,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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); - if (annotations) { - annotations.forEach(anno => { - anno.target = targetDoc; - }); - } - let pdfDoc = await Cast(annotationDoc.pdfDoc, Doc); - if (pdfDoc) { - DocUtils.MakeLink(annotationDoc, targetDoc, undefined, `Annotation from ${StrCast(pdfDoc.title)}`, "", StrCast(pdfDoc.title)); - } + annotations && annotations.forEach(anno => anno.target = targetDoc); + + DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`); } if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; @@ -524,7 +529,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action makeBackground = (): void => { - this.props.Document.isBackground = true; + this.props.Document.isBackground = !this.props.Document.isBackground; + this.props.Document.isBackground && this.props.bringToFront(this.props.Document, true); } @undoBatch @@ -533,6 +539,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.props.Document.lockedPosition = BoolCast(this.props.Document.lockedPosition) ? undefined : true; } + listen = async () => { + Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({ + continuous: { indefinite: true }, + interimHandler: (results: string) => { + let main = MainView.Instance; + main.dictationSuccess = true; + main.dictatedPhrase = results; + main.isListening = { interim: true }; + } + }); + } + @action onContextMenu = async (e: React.MouseEvent): Promise<void> => { e.persist(); @@ -553,12 +571,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" }); - cm.addItem({ description: "Pin to Presentation", event: () => PresBox.Instance.PinDoc(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle! - cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); - let makes: ContextMenuProps[] = []; - makes.push({ description: "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); + let existingMake = ContextMenu.Instance.findByDescription("Make..."); + let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; + makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" }); makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); + makes.push({ description: "Edit OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") }); makes.push({ description: "Make Portal", event: () => { let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); @@ -573,21 +590,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }, icon: "window-restore" }); - cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); - // cm.addItem({ - // description: "Find aliases", event: async () => { - // const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); - // this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? - // }, icon: "search" - // }); - if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) { - cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" }); - } - cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) }); + !existingMake && cm.addItem({ description: "Make...", subitems: makes, 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: "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" }); + } !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" }); if (!ClientUtils.RELEASE) { let copies: ContextMenuProps[] = []; @@ -595,6 +610,51 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" }); cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" }); } + let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); + let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : []; + analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" }); + !existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" }); + cm.addItem({ description: "Pin to Presentation", event: () => PresBox.Instance.PinDoc(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(); + } + }); + + cm.addItem({ + description: "Import document", icon: "upload", event: () => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const files = input.files; + if (!files) return; + const file = files[0]; + let formData = new FormData(); + formData.append('file', file); + formData.append('remap', "true"); + const upload = Utils.prepend("/uploadDoc"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json === "error") { + return; + } + const doc = await DocServer.GetRefField(json); + if (!doc || !(doc instanceof Doc)) { + return; + } + const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY); + doc.x = x, doc.y = y; + this.props.addDocument && this.props.addDocument(doc, false); + }; + input.click(); + } + }); cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); type User = { email: string, userDocumentId: string }; let usersMenu: ContextMenuProps[] = []; @@ -635,64 +695,70 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } - onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); }; + onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); }; 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; } @computed get contents() { return (<DocumentContentsView {...this.props} - isSelected={this.isSelected} select={this.select} + 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} />); } + chromeHeight = () => { + let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined; + let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle); + return showTitle ? 25 : 0; + } + 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() { - if (this.Document.hidden) { - return null; - } - let self = this; - let backgroundColor = StrCast(this.layoutDoc.backgroundColor); + 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 && showOverlays.title !== "undefined" ? showOverlays.title : StrCast(this.layoutDoc.showTitle); - let showCaption = showOverlays && showOverlays.caption !== "undefined" ? showOverlays.caption : StrCast(this.layoutDoc.showCaption); + 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 (str.indexOf("{props.Document.title}") !== -1) showTitle = "title"; - if (str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption"; + 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; return ( <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ - pointerEvents: this.layoutDoc.isBackground ? "none" : "all", + pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all", color: foregroundColor, - outlineColor: "maroon", - outlineStyle: "dashed", - outlineWidth: BoolCast(this.layoutDoc.libraryBrush) && !StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${this.props.ScreenToLocalTransform().Scale}px` : "0px", - marginLeft: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined, - marginTop: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `${-1 * this.props.ScreenToLocalTransform().Scale}px` : undefined, - border: BoolCast(this.layoutDoc.libraryBrush) && StrCast(Doc.GetProto(this.props.Document).borderRounding) ? - `dashed maroon ${this.props.ScreenToLocalTransform().Scale}px` : undefined, + 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", background: backgroundColor, width: nativeWidth, @@ -717,18 +783,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})` }}> <EditableView - contents={this.layoutDoc[showTitle]} + contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]} display={"block"} height={72} fontSize={12} - GetValue={() => StrCast(this.layoutDoc[showTitle!])} - SetValue={(value: string) => (Doc.GetProto(this.layoutDoc)[showTitle!] = value) ? true : true} + 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} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} selectOnLoad={this.props.selectOnLoad} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} /> + <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> } </div> diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/DragBox.scss new file mode 100644 index 000000000..fbb9b9c1c --- /dev/null +++ b/src/client/views/nodes/DragBox.scss @@ -0,0 +1,13 @@ +.dragBox-outerDiv { + width: 100%; + height: 100%; + pointer-events: all; + border-radius: inherit; + background: black; + border-radius: 100%; + svg { + margin:18%; + width:65% !important; + height:65%; + } +} diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx new file mode 100644 index 000000000..1f2c88086 --- /dev/null +++ b/src/client/views/nodes/DragBox.tsx @@ -0,0 +1,101 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit } from '@fortawesome/free-regular-svg-icons'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc } from '../../../new_fields/Doc'; +import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { ScriptField } from '../../../new_fields/ScriptField'; +import { emptyFunction } from '../../../Utils'; +import { CompileScript } from '../../util/Scripting'; +import { ContextMenu } from '../ContextMenu'; +import { DocComponent } from '../DocComponent'; +import { OverlayView } from '../OverlayView'; +import { ScriptBox } from '../ScriptBox'; +import { DocumentIconContainer } from './DocumentIcon'; +import './DragBox.scss'; +import { FieldView, FieldViewProps } from './FieldView'; +import { DragManager } from '../../util/DragManager'; +import { Docs } from '../../documents/Documents'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +library.add(faEdit as any); + +const DragSchema = createSchema({ + onDragStart: ScriptField, + text: "string" +}); + +type DragDocument = makeInterface<[typeof DragSchema]>; +const DragDocument = makeInterface(DragSchema); +@observer +export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocument) { + _downX: number = 0; + _downY: number = 0; + public static LayoutString() { return FieldView.LayoutString(DragBox); } + _mainCont = React.createRef<HTMLDivElement>(); + onDragStart = (e: React.PointerEvent) => { + if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) { + document.removeEventListener("pointermove", this.onDragMove); + document.addEventListener("pointermove", this.onDragMove); + document.removeEventListener("pointerup", this.onDragUp); + document.addEventListener("pointerup", this.onDragUp); + e.stopPropagation(); + e.preventDefault(); + } + } + + onDragMove = (e: MouseEvent) => { + if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) { + document.removeEventListener("pointermove", this.onDragMove); + document.removeEventListener("pointerup", this.onDragUp); + const onDragStart = this.Document.onDragStart; + e.stopPropagation(); + e.preventDefault(); + let res = onDragStart ? onDragStart.script.run({ this: this.props.Document }) : undefined; + let doc = res !== undefined && res.success ? + res.result as Doc : + Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" }); + doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY); + } + e.stopPropagation(); + e.preventDefault(); + } + + onDragUp = (e: MouseEvent) => { + document.removeEventListener("pointermove", this.onDragMove); + document.removeEventListener("pointerup", this.onDragUp); + } + + onContextMenu = () => { + ContextMenu.Instance.addItem({ + description: "Edit OnClick script", icon: "edit", event: () => { + let overlayDisposer: () => void = emptyFunction; + const script = this.Document.onDragStart; + let originalText: string | undefined = undefined; + if (script) originalText = script.script.originalScript; + // tslint:disable-next-line: no-unnecessary-callback-wrapper + let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + const script = CompileScript(text, { + params: { this: Doc.name }, + typecheck: false, + editable: true, + transformer: DocumentIconContainer.getTransformer() + }); + if (!script.compiled) { + onError(script.errors.map(error => error.messageText).join("\n")); + return; + } + this.Document.onClick = new ScriptField(script); + overlayDisposer(); + }} showDocumentIcons />; + overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` }); + } + }); + } + + render() { + return (<div className="dragBox-outerDiv" onContextMenu={this.onContextMenu} onPointerDown={this.onDragStart} ref={this._mainCont}> + <FontAwesomeIcon className="dragBox-icon" icon="folder" size="lg" color="white" /> + </div>); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 3570531b2..acf1aced3 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -20,7 +20,7 @@ export interface RectangleTemplate { export default class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { - let faces = DocListCast(Doc.GetProto(this.props.document).faces); + let faces = DocListCast(this.props.document.faces); let templates: RectangleTemplate[] = faces.map(faceDoc => { let rectangle = Cast(faceDoc.faceRectangle, Doc) as Doc; let style = { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index b1030d19d..cae975f30 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -17,8 +17,7 @@ import { IconBox } from "./IconBox"; import { ImageBox } from "./ImageBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { PresBox } from "./PresBox"; +import { ScriptField } from "../../../new_fields/ScriptField"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -33,6 +32,7 @@ export interface FieldViewProps { ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; Document: Doc; DataDoc?: Doc; + onClick?: ScriptField; isSelected: () => boolean; select: (isCtrlPressed: boolean) => void; renderDepth: number; @@ -49,6 +49,8 @@ export interface FieldViewProps { PanelHeight: () => number; setVideoBox?: (player: VideoBox) => void; setPdfBox?: (player: PDFBox) => void; + ContentScaling: () => number; + ChromeHeight?: () => number; } @observer diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index a24abb32e..1b537cc52 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -33,17 +33,17 @@ } .formattedTextBox-inner-rounded { - height: calc(100% - 25px); - width: calc(100% - 40px); + height: 70%; + width: 85%; position: absolute; overflow: auto; - top: 15; - left: 20; + top: 15%; + left: 10%; } .formattedTextBox-inner-rounded div, .formattedTextBox-inner div { - padding: 10px; + padding: 10px 10px; } .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f019868aa..f7890e5a6 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,19 +1,21 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons'; -import { action, IReactionDisposer, observable, reaction, runInAction, computed, trace } from "mobx"; +import { action, computed, IReactionDisposer, Lambda, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { EditorState, Plugin, Transaction, Selection } from "prosemirror-state"; -import { NodeType, Slice, Node, Fragment } from 'prosemirror-model'; +import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model"; +import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Doc, Opt, DocListCast } from "../../../new_fields/Doc"; -import { Id, Copy } from '../../../new_fields/FieldSymbols'; +import { DateField } from '../../../new_fields/DateField'; +import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; import { RichTextField } from "../../../new_fields/RichTextField"; -import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils } from '../../../Utils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; @@ -25,16 +27,12 @@ import { SelectionManager } from "../../util/SelectionManager"; import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { ContextMenu } from "../../views/ContextMenu"; -import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { Templates } from '../Templates'; +import { MainOverlayTextBox } from '../MainOverlayTextBox'; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); -import { DateField } from '../../../new_fields/DateField'; -import { Utils } from '../../../Utils'; library.add(faEdit); library.add(faSmile, faTextHeight); @@ -48,6 +46,7 @@ export interface FormattedTextBoxProps { height?: string; color?: string; outer_div?: (domminus: HTMLElement) => void; + firstinstance?: boolean; } const richTextSchema = createSchema({ @@ -62,15 +61,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static Instance: FormattedTextBox; private _ref: React.RefObject<HTMLDivElement>; - private _outerdiv?: (dominus: HTMLElement) => void; private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; - private _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _applyingChange: boolean = false; private _linkClicked = ""; private _reactionDisposer: Opt<IReactionDisposer>; + private _searchReactionDisposer?: Lambda; private _textReactionDisposer: Opt<IReactionDisposer>; + private _heightReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -99,6 +100,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return ""; } + public static getToolTip() { + return this._toolTipTextMenu; + } + @undoBatch public setFontColor(color: string) { let self = this; @@ -114,10 +119,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); - if (this.props.outer_div) { - this._outerdiv = this.props.outer_div; - } - + FormattedTextBox.Instance = this; this._ref = React.createRef(); if (this.props.isOverlay) { DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); @@ -128,7 +130,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); } - @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } + paste = (e: ClipboardEvent) => { if (e.clipboardData && this._editorView) { @@ -144,7 +147,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe // tx.setSelection(new Selection(tx.)) const state = this._editorView!.state; this._editorView!.dispatch(tx); - if (this._toolTipTextMenu) { + if (FormattedTextBox._toolTipTextMenu) { // this._toolTipTextMenu.makeLinkWithState(state) } e.stopPropagation(); @@ -159,8 +162,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe dispatchTransaction = (tx: Transaction) => { if (this._editorView) { const state = this._editorView.state.apply(tx); + FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true); this._editorView.updateState(state); + FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false); + if (state.selection.empty && FormattedTextBox._toolTipTextMenu) { + const marks = tx.storedMarks; + console.log(marks); + if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); } + } this._applyingChange = true; + const fieldkey = "preview"; if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now())); this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); @@ -174,6 +185,47 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + public highlightSearchTerms = (terms: String[]) => { + if (this._editorView && (this._editorView as any).docView) { + const fieldkey = "preview"; + const doc = this._editorView.state.doc; + const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + if (node.isLeaf && node.isText && node.text) { + let nodeText: String = node.text; + let tokens = nodeText.split(" "); + let start = pos; + tokens.forEach((word) => { + if (terms.includes(word) && this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark)); + } + start += word.length + 1; + }); + } + }); + } + } + + public unhighlightSearchTerms = () => { + if (this._editorView && (this._editorView as any).docView) { + const doc = this._editorView.state.doc; + const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); + doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => { + if (node.isLeaf && node.isText && node.text) { + if (node.marks.includes(mark) && this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark)); + } + } + }); + // const fieldkey = 'search_string'; + // if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { + // this.props.Document[fieldkey] = undefined; + // } + // else this.props.Document.proto![fieldkey] = undefined; + // } + } + } + protected createDropTarget = (ele: HTMLDivElement) => { this._proseRef = ele; if (this.dropDisposer) { @@ -199,25 +251,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.props.Document.layout = de.data.draggedDocuments[0]; de.data.draggedDocuments[0].isTemplate = true; e.stopPropagation(); - // let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); - // if (!ldocs) { - // this.dataDoc.subBulletDocs = new List<Doc>([]); - // } - // ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc)); - // if (!ldocs) return; - // if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) { - // ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 })); - // this.props.addDocument && this.props.addDocument(ldocs[0] as Doc); - // this.props.Document.templates = new List<string>([Templates.Bullet.Layout]); - // this.props.Document.isBullet = true; - // } - // let stackDoc = (ldocs[0] as Doc); - // if (de.data.moveDocument) { - // de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => { - // Cast(stackDoc.data, listSpec(Doc))!.push(doc); - // return true; - // }); - // } } } } @@ -259,12 +292,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, - field2 => { - this._editorView && !this._applyingChange && - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); - } + field2 => this._editorView && !this._applyingChange && + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))) ); + this.props.isOverlay && (this._heightReactionDisposer = reaction( + () => this.props.Document[WidthSym](), + () => this.tryUpdateHeight() + )); + this._textReactionDisposer = reaction( () => this.extensionDoc, () => { @@ -276,6 +312,22 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }, { fireImmediately: true }); this.setupEditor(config, this.dataDoc, this.props.fieldKey); + + this._searchReactionDisposer = reaction(() => { + return StrCast(this.props.Document.search_string); + }, searchString => { + const fieldkey = 'preview'; + let preview = false; + // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) { + // preview = true; + // } + if (searchString) { + this.highlightSearchTerms([searchString]); + } + else { + this.unhighlightSearchTerms(); + } + }, { fireImmediately: true }); } clipboardTextSerializer = (slice: Slice): string => { @@ -328,7 +380,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe fieldExtDoc.annotations = new List<Doc>(targetAnnotations); } - let link = DocUtils.MakeLink(this.props.Document, region); + let link = DocUtils.MakeLink(this.props.Document, region, doc); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); linkId = link[Id]; @@ -382,7 +434,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe dispatchTransaction: this.dispatchTransaction, nodeViews: { image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }, - star(node, view, getPos) { return new SummarizedView(node, view, getPos); } + star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, @@ -405,12 +457,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); this._textReactionDisposer && this._textReactionDisposer(); + this._heightReactionDisposer && this._heightReactionDisposer(); + this._searchReactionDisposer && this._searchReactionDisposer(); } onPointerDown = (e: React.PointerEvent): void => { + if (this.props.onClick && e.button === 0) { + e.preventDefault(); + } if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { e.stopPropagation(); - if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { //this._toolTipTextMenu.tooltip.style.opacity = "0"; } } @@ -442,6 +499,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } if (targetContext) { DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } else if (jumpToDoc) { + DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab")); + } } }); @@ -463,7 +523,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } onPointerUp = (e: React.PointerEvent): void => { - if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { + if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) { //this._toolTipTextMenu.tooltip.style.opacity = "1"; } if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { @@ -504,7 +564,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe tooltipTextMenuPlugin() { let myprops = this.props; - let self = this; + let self = FormattedTextBox; return new Plugin({ view(_editorView) { return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops); @@ -550,14 +610,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @action tryUpdateHeight() { - if (this.props.isOverlay && this.props.Document.autoHeight) { - let self = this; + if (this.props.Document.autoHeight && this.props.isOverlay) { let xf = this._ref.current!.getBoundingClientRect(); let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height); - let nh = NumCast(this.dataDoc.nativeHeight, 0); + let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); let sh = scrBounds.height; - this.props.Document.height = nh ? dh / nh * sh : sh; + const ChromeHeight = MainOverlayTextBox.Instance.ChromeHeight; + this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); this.dataDoc.nativeHeight = nh ? sh : undefined; } } @@ -572,27 +632,29 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } specificContextMenu = (e: React.MouseEvent): void => { - let subitems: ContextMenuProps[] = []; - subitems.push({ - description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height", - event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" - }); - ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); + // let subitems: ContextMenuProps[] = []; + // subitems.push({ + // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height", + // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" + // }); + // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); } render() { let self = this; let style = this.props.isOverlay ? "scroll" : "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; - let interactive = InkingControl.Instance.selectedTool ? "" : "interactive"; + let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground || + (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all"; Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); return ( <div className={`formattedTextBox-cont-${style}`} ref={this._ref} style={{ + overflowY: this.props.Document.autoHeight ? "hidden" : "auto", height: this.props.height ? this.props.height : undefined, - background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined, - opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1, + background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, + opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", - pointerEvents: interactive ? "all" : "none", + pointerEvents: interactive, fontSize: "13px" }} onKeyDown={this.onKeyPress} @@ -603,12 +665,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onMouseDown={this.onMouseDown} - // tfs: do we need this event handler onWheel={this.onPointerWheel} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} > - <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} /> + <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} /> </div> ); } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 697f19f0d..b1afa3f7d 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -56,4 +56,46 @@ left: 5%; top: 15%; } -}
\ No newline at end of file +} + +#cf { + position:relative; + width:100%; + margin:0 auto; + display:flex; + align-items: center; + height:100%; + .imageBox-fadeBlocker { + width:100%; + height:100%; + background: black; + display:flex; + flex-direction: row; + align-items: center; + z-index: 1; + .imageBox-fadeaway { + object-fit: contain; + width:100%; + height:100%; + } + } + } + + #cf img { + position:absolute; + left:0; + } + + .imageBox-fadeBlocker { + -webkit-transition: opacity 1s ease-in-out; + -moz-transition: opacity 1s ease-in-out; + -o-transition: opacity 1s ease-in-out; + transition: opacity 1s ease-in-out; + } + .imageBox-fadeBlocker:hover { + -webkit-transition: opacity 1s ease-in-out; + -moz-transition: opacity 1s ease-in-out; + -o-transition: opacity 1s ease-in-out; + transition: opacity 1s ease-in-out; + opacity:0; + }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 29a76b0c8..708e00576 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,39 +1,39 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faImage, faFileAudio, faPaintBrush, faAsterisk } from '@fortawesome/free-solid-svg-icons'; -import { action, observable, computed, runInAction } from 'mobx'; +import { faEye } from '@fortawesome/free-regular-svg-icons'; +import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { Doc, HeightSym, WidthSym, DocListCast } from '../../../new_fields/Doc'; +import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; -import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types'; -import { ImageField, AudioField } from '../../../new_fields/URLField'; +import { ComputedField } from '../../../new_fields/ScriptField'; +import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; +import { AudioField, ImageField } from '../../../new_fields/URLField'; +import { RouteStore } from '../../../server/RouteStore'; import { Utils } from '../../../Utils'; +import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; +import { CompileScript } from '../../util/Scripting'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { InkingControl } from '../InkingControl'; import { positionSchema } from './DocumentView'; +import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { RouteStore } from '../../../server/RouteStore'; -import { Docs, DocumentType } from '../../documents/Documents'; -import { DocServer } from '../../DocServer'; -import { Font } from '@react-pdf/renderer'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CognitiveServices } from '../../cognitive_services/CognitiveServices'; -import FaceRectangles from './FaceRectangles'; -import { faEye } from '@fortawesome/free-regular-svg-icons'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye, faPaintBrush); +library.add(faImage, faEye as any, faPaintBrush); library.add(faFileAudio, faAsterisk); @@ -65,7 +65,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private dropDisposer?: DragManager.DragDropDisposer; - @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } protected createDropTarget = (ele: HTMLDivElement) => { @@ -90,18 +90,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD if (de.data instanceof DragManager.DocumentDragData) { de.data.droppedDocuments.forEach(action((drop: Doc) => { if (de.mods === "CtrlKey") { - let temp = Doc.MakeDelegate(drop); - this.props.Document.nativeWidth = Doc.GetProto(this.props.Document).nativeWidth = undefined; - this.props.Document.nativeHeight = Doc.GetProto(this.props.Document).nativeHeight = undefined; - this.props.Document.width = drop.width; - this.props.Document.height = drop.height; - Doc.GetProto(this.props.Document).type = DocumentType.TEMPLATE; - this.props.Document.layout = temp; + Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc); e.stopPropagation(); } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) { Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url); e.stopPropagation(); - } else if (de.mods === "CtrlKey") { + } else if (de.mods === "MetaKey") { if (this.extensionDoc !== this.dataDoc) { let layout = StrCast(drop.backgroundLayout); if (layout.indexOf(ImageBox.name) !== -1) { @@ -226,20 +220,60 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); let modes: ContextMenuProps[] = []; - let dataDoc = Doc.GetProto(this.props.Document); - modes.push({ description: "Generate Tags", event: () => CognitiveServices.Image.generateMetadata(dataDoc), icon: "tag" }); - modes.push({ description: "Find Faces", event: () => CognitiveServices.Image.extractFaces(dataDoc), icon: "camera" }); + modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" }); + modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" }); ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" }); ContextMenu.Instance.addItem({ description: "Analyze...", subitems: modes, icon: "eye" }); } } + extractFaces = () => { + let converter = (results: any) => { + let faceDocs = new List<Doc>(); + results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); + return faceDocs; + }; + if (this.url) { + CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); + } + } + + generateMetadata = (threshold: Confidence = Confidence.Excellent) => { + let converter = (results: any) => { + let tagDoc = new Doc; + let tagsList = new List(); + results.tags.map((tag: Tag) => { + tagsList.push(tag.name); + let sanitized = tag.name.replace(" ", "_"); + let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`; + let computed = CompileScript(script, { params: { this: "Doc" } }); + computed.compiled && (tagDoc[sanitized] = new ComputedField(computed)); + }); + this.extensionDoc.generatedTags = tagsList; + tagDoc.title = "Generated Tags Doc"; + tagDoc.confidence = threshold; + return tagDoc; + }; + if (this.url) { + CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); + } + } + @action onDotDown(index: number) { this.Document.curPage = index; } + @computed get fieldExtensionDoc() { + return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); + } + + @computed private get url() { + let data = Cast(Doc.GetProto(this.props.Document).data, ImageField); + return data ? data.url.href : undefined; + } + dots(paths: string[]) { let nativeWidth = FieldValue(this.Document.nativeWidth, 1); let dist = Math.min(nativeWidth / paths.length, 40); @@ -284,14 +318,14 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD resize(srcpath: string, layoutdoc: Doc) { requestImageSize(srcpath) .then((size: any) => { - let aspect = size.height / size.width; let rotation = NumCast(this.dataDoc.rotation) % 180; - if (rotation === 90 || rotation === 270) aspect = 1 / aspect; - if (Math.abs(layoutdoc[HeightSym]() / layoutdoc[WidthSym]() - aspect) > 0.01) { + let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; + let aspect = realsize.height / realsize.width; + if (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1) { setTimeout(action(() => { layoutdoc.height = layoutdoc[WidthSym]() * aspect; - layoutdoc.nativeHeight = size.height; - layoutdoc.nativeWidth = size.width; + layoutdoc.nativeHeight = realsize.height; + layoutdoc.nativeWidth = realsize.width; }), 0); } }) @@ -370,6 +404,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)]; + let fadepath = paths[Math.min(paths.length - 1, 1)]; if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); @@ -377,13 +412,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> - <img id={id} - key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys - src={srcpath} - style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} - width={nativeWidth} - ref={this._imgRef} - onError={this.onError} /> + <div id="cf"> + <img id={id} + key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + src={srcpath} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + width={nativeWidth} + ref={this._imgRef} + onError={this.onError} /> + {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> <img className="imageBox-fadeaway" + key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + src={fadepath} + style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }} + width={nativeWidth} + ref={this._imgRef} + onError={this.onError} /></div>} + </div> {paths.length > 1 ? this.dots(paths) : (null)} <div className="imageBox-audioBackground" onPointerDown={this.audioDown} @@ -394,7 +438,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> </div> {/* {this.lightbox(paths)} */} - <FaceRectangles document={this.props.Document} color={"#0000FF"} backgroundColor={"#0000FF"} /> + <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 77824b4ff..0d4b377dd 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,8 @@ import { RichTextField } from "../../../new_fields/RichTextField"; import { ImageField } from "../../../new_fields/URLField"; import { SelectionManager } from "../../util/SelectionManager"; import { listSpec } from "../../../new_fields/Schema"; +import { CollectionViewType } from "../collections/CollectionBaseView"; +import { undoBatch } from "../../util/UndoManager"; export type KVPScript = { script: CompiledScript; @@ -71,6 +73,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean { const { script, type, onDelegate } = kvpScript; + //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates const target = onDelegate ? doc : Doc.GetProto(doc); let field: Field; if (type === "computed") { @@ -89,6 +92,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return false; } + @undoBatch public static SetField(doc: Doc, key: string, value: string) { const script = this.CompileKVPScript(value); if (!script) return false; @@ -195,6 +199,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } let fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey); + if (!fieldTemplate) { + return; + } let previousViewType = fieldTemplate.viewType; Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc)); previousViewType && (fieldTemplate.viewType = previousViewType); @@ -211,14 +218,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return Docs.Create.StackingDocument([], options); } let first = await Cast(data[0], Doc); - if (!first) { + if (!first || !first.data) { return Docs.Create.StackingDocument([], options); } - switch (first.type) { - case "image": - return Docs.Create.StackingDocument([], options); - case "text": + switch (first.data.constructor) { + case RichTextField: return Docs.Create.TreeDocument([], options); + case ImageField: + return Docs.Create.MasonryDocument([], options); + default: + console.log(`Template for ${first.data.constructor} not supported!`); + return undefined; } } else if (data instanceof ImageField) { return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 064f3edcc..534a42efc 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,21 +1,19 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils'; -import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting"; +import { Doc, Field } from '../../../new_fields/Doc'; +import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; +import { ContextMenu } from '../ContextMenu'; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from './FieldView'; +import { KeyValueBox } from './KeyValueBox'; import "./KeyValueBox.scss"; import "./KeyValuePair.scss"; import React = require("react"); -import { Doc, Opt, Field } from '../../../new_fields/Doc'; -import { FieldValue } from '../../../new_fields/Types'; -import { KeyValueBox } from './KeyValueBox'; -import { DragManager, SetupDrag } from '../../util/DragManager'; -import { ContextMenu } from '../ContextMenu'; -import { Docs } from '../../documents/Documents'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; // Represents one row in a key value plane @@ -70,6 +68,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { PanelWidth: returnZero, PanelHeight: returnZero, addDocTab: returnZero, + ContentScaling: returnOne }; let contents = <FieldView {...props} />; // let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")"; @@ -91,12 +90,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}> <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> - <button style={hover} className="keyValuePair-td-key-delete" onClick={() => { + <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { props.Document[props.fieldKey] = undefined; } else props.Document.proto![props.fieldKey] = undefined; - }}> + })}> X </button> <input diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index 0ea948c81..ecb3e9db4 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -290,7 +290,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); let index = keys.indexOf(""); if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c)); + let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx index 0cb216aa6..e04044266 100644 --- a/src/client/views/nodes/LinkMenuGroup.tsx +++ b/src/client/views/nodes/LinkMenuGroup.tsx @@ -72,7 +72,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType); let index = keys.indexOf(""); if (index > -1) keys.splice(index, 1); - let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c)); + let cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb")); let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx index 1d4fcad69..a119eb39b 100644 --- a/src/client/views/nodes/LinkMenuItem.tsx +++ b/src/client/views/nodes/LinkMenuItem.tsx @@ -6,7 +6,7 @@ import { DocumentManager } from "../../util/DocumentManager"; import { undoBatch } from "../../util/UndoManager"; import './LinkMenu.scss'; import React = require("react"); -import { Doc } from '../../../new_fields/Doc'; +import { Doc, DocListCastAsync } from '../../../new_fields/Doc'; import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types'; import { observable, action } from 'mobx'; import { LinkManager } from '../../util/LinkManager'; @@ -52,7 +52,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { } if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) { - DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(targetContext!)); + DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext!); } else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) { DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!)); diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index e7655d598..c88a94c28 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -1,37 +1,3 @@ -.react-pdf__Page { - transform-origin: left top; - position: absolute; - top: 0; - left: 0; -} - -.react-pdf__Page__textContent span { - user-select: text; -} - -.react-pdf__Document { - position: absolute; -} - -.pdfBox-buttonTray { - position: absolute; - top: 0; - left: 0; - z-index: 25; - pointer-events: all; -} - -.pdfBox-thumbnail { - position: absolute; - width: 100%; -} - -.pdfButton { - pointer-events: all; - width: 100px; - height: 100px; -} - .pdfBox-cont, .pdfBox-cont-interactive { display: flex; @@ -39,30 +5,24 @@ height: 100%; overflow-y: scroll; overflow-x: hidden; + .pdfBox-scrollHack { + pointer-events: none; + } } .pdfBox-cont { pointer-events: none; - - .textlayer { - pointer-events: none; - + .pdfPage-textlayer { span { pointer-events: none !important; + user-select: none; } } - - .page-cont { - pointer-events: none; - } } .pdfBox-cont-interactive { pointer-events: all; - display: flex; - flex-direction: row; - - .textlayer { + .pdfPage-textlayer { span { pointer-events: all !important; user-select: text; @@ -70,11 +30,22 @@ } } -.pdfBox-contentContainer { - position: absolute; +.react-pdf__Page { transform-origin: left top; + position: absolute; + top: 0; + left: 0; } +.react-pdf__Page__textContent span { + user-select: text; +} + +.react-pdf__Document { + position: absolute; +} + + .pdfBox-settingsCont { position: absolute; right: 0; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 4973340df..6450cb826 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,31 +1,24 @@ -import { action, IReactionDisposer, observable, reaction, trace, untracked, computed } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from "mobx-react"; +import * as Pdfjs from "pdfjs-dist"; +import "pdfjs-dist/web/pdf_viewer.css"; import 'react-image-lightbox/style.css'; -import { WidthSym, Doc } from "../../../new_fields/Doc"; +import { Doc, WidthSym, Opt } from "../../../new_fields/Doc"; import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, NumCast, BoolCast } from "../../../new_fields/Types"; +import { ScriptField } from '../../../new_fields/ScriptField'; +import { BoolCast, Cast, NumCast } from "../../../new_fields/Types"; import { PdfField } from "../../../new_fields/URLField"; -//@ts-ignore -// import { Document, Page } from "react-pdf"; -// import 'react-pdf/dist/Page/AnnotationLayer.css'; -import { RouteStore } from "../../../server/RouteStore"; +import { KeyCodes } from '../../northstar/utils/KeyCodes'; +import { CompileScript } from '../../util/Scripting'; import { DocComponent } from "../DocComponent"; import { InkingControl } from "../InkingControl"; -import { FilterBox } from "../search/FilterBox"; -import { Annotation } from './Annotation'; import { PDFViewer } from "../pdf/PDFViewer"; import { positionSchema } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); -import { CompileScript } from '../../util/Scripting'; -import { Flyout, anchorPoints } from '../DocumentDecorations'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ScriptField } from '../../../new_fields/ScriptField'; -import { KeyCodes } from '../../northstar/utils/KeyCodes'; -import { Utils } from '../../../Utils'; -import { Id } from '../../../new_fields/FieldSymbols'; type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const PdfDocument = makeInterface(positionSchema, pageSchema); @@ -35,241 +28,179 @@ export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === K export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString() { return FieldView.LayoutString(PDFBox); } + @observable private _flyout: boolean = false; @observable private _alt = false; - @observable private _scrollY: number = 0; + @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; + + @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; } @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } + @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); } - @observable private _flyout: boolean = false; - private _mainCont: React.RefObject<HTMLDivElement>; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _reactionDisposer?: IReactionDisposer; private _keyValue: string = ""; private _valueValue: string = ""; private _scriptValue: string = ""; - private _keyRef: React.RefObject<HTMLInputElement>; - private _valueRef: React.RefObject<HTMLInputElement>; - private _scriptRef: React.RefObject<HTMLInputElement>; + private _keyRef: React.RefObject<HTMLInputElement> = React.createRef(); + private _valueRef: React.RefObject<HTMLInputElement> = React.createRef(); + private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef(); - constructor(props: FieldViewProps) { - super(props); + componentDidMount() { + this.props.setPdfBox && this.props.setPdfBox(this); - this._mainCont = React.createRef(); + const pdfUrl = Cast(this.props.Document.data, PdfField); + if (pdfUrl instanceof PdfField) { + Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf)); + } this._reactionDisposer = reaction( - () => this.props.Document.scrollY, - () => { - if (this._mainCont.current) { - this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.scrollY), behavior: "auto" }); - } - } + () => this.props.Document.panY, + () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" }) ); - - this._keyRef = React.createRef(); - this._valueRef = React.createRef(); - this._scriptRef = React.createRef(); - } - - componentDidMount() { - if (this.props.setPdfBox) this.props.setPdfBox(this); - - document.removeEventListener("copy", this.copy); - document.addEventListener("copy", this.copy); } componentWillUnmount() { this._reactionDisposer && this._reactionDisposer(); - document.removeEventListener("copy", this.copy); - } - - private copy = (e: ClipboardEvent) => { - if (this.props.active()) { - if (e.clipboardData) { - e.clipboardData.setData("text/plain", text); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.preventDefault(); - } - } } public GetPage() { - return Math.floor(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; + return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; } + + @action public BackPage() { - let cp = Math.ceil(NumCast(this.props.Document.scrollY) / NumCast(this.dataDoc.pdfHeight)) + 1; + let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1; cp = cp - 1; if (cp > 0) { this.props.Document.curPage = cp; - this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); + this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); } } + + @action public GotoPage(p: number) { if (p > 0 && p <= NumCast(this.props.Document.numPages)) { this.props.Document.curPage = p; - this.props.Document.scrollY = (p - 1) * NumCast(this.dataDoc.pdfHeight); + this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight); } } + @action public ForwardPage() { let cp = this.GetPage() + 1; if (cp <= NumCast(this.props.Document.numPages)) { this.props.Document.curPage = cp; - this.props.Document.scrollY = (cp - 1) * NumCast(this.dataDoc.pdfHeight); + this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight); } } - private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._keyValue = e.currentTarget.value; - } - - private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._valueValue = e.currentTarget.value; - } - @action - private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => { - this._scriptValue = e.currentTarget.value; + setPanY = (y: number) => { + this.containingCollectionDocument && (this.containingCollectionDocument.panY = y); } + @action private applyFilter = () => { - let scriptText = ""; - if (this._scriptValue.length > 0) { - scriptText = this._scriptValue; - } else if (this._keyValue.length > 0 && this._valueValue.length > 0) { - scriptText = `return this.${this._keyValue} === ${this._valueValue}`; - } - else { - scriptText = "return true"; - } + let scriptText = this._scriptValue.length > 0 ? this._scriptValue : + this._keyValue.length > 0 && this._valueValue.length > 0 ? + `return this.${this._keyValue} === ${this._valueValue}` : "return true"; let script = CompileScript(scriptText, { params: { this: Doc.name } }); - if (script.compiled) { - this.props.Document.filterScript = new ScriptField(script); - } + script.compiled && (this.props.Document.filterScript = new ScriptField(script)); } - @action - private toggleFlyout = () => { - this._flyout = !this._flyout; + scrollTo = (y: number) => { + this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" }); } - @action private resetFilters = () => { this._keyValue = this._valueValue = ""; this._scriptValue = "return true"; - if (this._keyRef.current) { - this._keyRef.current.value = ""; - } - if (this._valueRef.current) { - this._valueRef.current.value = ""; - } - if (this._scriptRef.current) { - this._scriptRef.current.value = ""; - } + this._keyRef.current && (this._keyRef.current.value = ""); + this._valueRef.current && (this._valueRef.current.value = ""); + this._scriptRef.current && (this._scriptRef.current.value = ""); this.applyFilter(); } - - scrollTo(y: number) { - if (this._mainCont.current) { - this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current!.offsetHeight / 2), 0), behavior: "auto" }); - } - } + private newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => this._keyValue = e.currentTarget.value; + private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value; + private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value; settingsPanel() { return !this.props.active() ? (null) : - ( - <div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> - <button className="pdfBox-settingsButton" onClick={this.toggleFlyout} title="Open Annotation Settings" - style={{ marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` }}> - <div className="pdfBox-settingsButton-arrow" - style={{ - borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, - borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, - borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, - transform: `scaleX(${this._flyout ? -1 : 1})` - }}></div> - <div className="pdfBox-settingsButton-iconCont"> - <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> - </div> - </button> - <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > - <div className="pdfBox-settingsFlyout-title"> - Annotation View Settings - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange} - style={{ gridColumn: 1 }} ref={this._keyRef} /> - <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange} - style={{ gridColumn: 3 }} ref={this._valueRef} /> - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> - </div> - <div className="pdfBox-settingsFlyout-kvpInput"> - <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> - <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> - Reset Filters - </button> - <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> - <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> - Apply - </button> - </div> + (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}> + <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" + style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}> + <div className="pdfBox-settingsButton-arrow" + style={{ + borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`, + borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`, + borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`, + transform: `scaleX(${this._flyout ? -1 : 1})` + }} /> + <div className="pdfBox-settingsButton-iconCont"> + <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" /> + </div> + </button> + <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} > + <div className="pdfBox-settingsFlyout-title"> + Annotation View Settings + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange} + style={{ gridColumn: 1 }} ref={this._keyRef} /> + <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange} + style={{ gridColumn: 3 }} ref={this._valueRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} /> + </div> + <div className="pdfBox-settingsFlyout-kvpInput"> + <button style={{ gridColumn: 1 }} onClick={this.resetFilters}> + <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" /> + Reset Filters + </button> + <button style={{ gridColumn: 3 }} onClick={this.applyFilter}> + <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" /> + Apply + </button> </div> </div> - ); + </div>); } loaded = (nw: number, nh: number, np: number) => { - if (this.props.Document) { - let doc = this.dataDoc; - doc.numPages = np; - if (doc.nativeWidth && doc.nativeHeight) return; - let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); - doc.nativeWidth = nw; - if (doc.nativeHeight) doc.nativeHeight = nw * oldaspect; - else doc.nativeHeight = nh; - let ccv = this.props.ContainingCollectionView; - if (ccv) { - ccv.props.Document.pdfHeight = nh; - } - doc.height = nh * (doc[WidthSym]() / nw); + this.dataDoc.numPages = np; + if (!this.dataDoc.nativeWidth || !this.dataDoc.nativeHeight || !this.dataDoc.scrollHeight) { + let oldaspect = NumCast(this.dataDoc.nativeHeight) / NumCast(this.dataDoc.nativeWidth, 1); + this.dataDoc.nativeWidth = nw; + this.dataDoc.nativeHeight = this.dataDoc.nativeHeight ? nw * oldaspect : nh; + this.dataDoc.height = this.dataDoc[WidthSym]() * (nh / nw); + this.dataDoc.scrollHeight = np * this.dataDoc.nativeHeight; } } @action onScroll = (e: React.UIEvent<HTMLDivElement>) => { - - if (e.currentTarget) { - this._scrollY = e.currentTarget.scrollTop; - let ccv = this.props.ContainingCollectionView; - if (ccv) { - ccv.props.Document.panTransformType = "None"; - ccv.props.Document.scrollY = this._scrollY; - } + if (e.currentTarget && this.containingCollectionDocument) { + this.containingCollectionDocument.panTransformType = "None"; + this.containingCollectionDocument.panY = e.currentTarget.scrollTop; } } - - @computed get fieldExtensionDoc() { - return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); - } render() { - // uses mozilla pdf as default const pdfUrl = Cast(this.props.Document.data, PdfField); - if (!(pdfUrl instanceof PdfField)) return <div>{`pdf, ${this.props.Document.data}, not found`}</div>; let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); - return ( + return (!(pdfUrl instanceof PdfField) || !this._pdf ? + <div>{`pdf, ${this.props.Document.data}, not found`}</div> : <div className={classname} onScroll={this.onScroll} - style={{ - marginTop: `${NumCast(this.props.ContainingCollectionView!.props.Document.panY)}px` - }} - ref={this._mainCont} - onWheel={(e: React.WheelEvent) => { - e.stopPropagation(); - }}> - <PDFViewer url={pdfUrl.url.pathname} loaded={this.loaded} scrollY={this._scrollY} parent={this} /> - {/* <div style={{ width: "100px", height: "300px" }}></div> */} + style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }} + ref={this._mainCont}> + <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale, 1)), width: "100%" }} /> + <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)} + Document={this.props.Document} DataDoc={this.props.DataDoc} + addDocTab={this.props.addDocTab} setPanY={this.setPanY} + addDocument={this.props.addDocument} + fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} /> {this.settingsPanel()} - </div> - ); + </div>); } - }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 34cb47b20..704030d85 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -8,7 +8,6 @@ import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from "../../../Utils"; -import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from "../../documents/Documents"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; @@ -21,6 +20,10 @@ import { pageSchema } from "./ImageBox"; import "./VideoBox.scss"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faVideo } from "@fortawesome/free-solid-svg-icons"; +import { CompileScript } from "../../util/Scripting"; +import { Doc } from "../../../new_fields/Doc"; +import { ScriptField } from "../../../new_fields/ScriptField"; +var path = require('path'); type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -87,6 +90,63 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"); } + choosePath(url: string) { + if (url.indexOf(window.location.origin) === -1) { + return Utils.CorsProxy(url); + } + return url; + } + + @action public Snapshot() { + let width = NumCast(this.props.Document.width); + let height = NumCast(this.props.Document.height); + var canvas = document.createElement('canvas'); + canvas.width = 640; + canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); + var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions + if (ctx) { + ctx.rect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "blue"; + ctx.fill(); + this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); + } + + if (!this._videoRef) { // can't find a way to take snapshots of videos + let b = Docs.Create.ButtonDocument({ + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString() + }); + const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, { + params: { this: Doc.name }, + capturedVariables: { self: this.props.Document }, + typecheck: false, + editable: true, + }); + if (script.compiled) { + b.onClick = new ScriptField(script); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false); + } else { + console.log(script.errors.map(error => error.messageText).join("\n")); + } + } else { + //convert to desired file format + var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' + // if you want to preview the captured image, + let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); + VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { + if (returnedFilename) { + let url = this.choosePath(Utils.prepend(returnedFilename)); + let imageSummary = Docs.Create.ImageDocument(url, { + x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), + width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" + }); + this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); + DocUtils.MakeLink(imageSummary, this.props.Document); + } + }); + } + } + @action updateTimecode = () => { this.player && (this.props.Document.curPage = this.player.currentTime); @@ -150,39 +210,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD let subitems: ContextMenuProps[] = []; subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" }); subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" }); - let width = NumCast(this.props.Document.width); - let height = NumCast(this.props.Document.height); - subitems.push({ - description: "Take Snapshot", event: async () => { - var canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth); - var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions - if (ctx) { - ctx.rect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = "blue"; - ctx.fill(); - this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height); - } - - //convert to desired file format - var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' - // if you want to preview the captured image, - let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, ""); - VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => { - if (returnedFilename) { - let url = Utils.prepend(returnedFilename); - let imageSummary = Docs.Create.ImageDocument(url, { - x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), - width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" - }); - this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false); - DocUtils.MakeLink(imageSummary, this.props.Document); - } - }); - }, - icon: "expand-arrows-alt" - }); + subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" }); } } diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index eb09b0693..07774263c 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -1,16 +1,18 @@ - -.webBox-cont, .webBox-cont-interactive{ +.webBox-cont, +.webBox-cont-interactive { padding: 0vw; position: absolute; top: 0; - left:0; + left: 0; width: 100%; height: 100%; overflow: auto; - pointer-events: none ; + pointer-events: none; } + .webBox-cont-interactive { pointer-events: all; + span { user-select: text !important; } @@ -18,8 +20,8 @@ #webBox-htmlSpan { position: absolute; - top:0; - left:0; + top: 0; + left: 0; } .webBox-overlay { @@ -29,8 +31,52 @@ } .webBox-button { - padding : 0vw; + padding: 0vw; border: none; - width : 100%; + width: 100%; + height: 100%; +} + +.webView-urlEditor { + position: relative; + opacity: 0.9; + z-index: 9001; + transition: top .5s; + background: lightgrey; + padding: 10px; + + + .urlEditor { + display: grid; + grid-template-columns: 1fr auto; + padding-bottom: 10px; + overflow: hidden; + + .editorBase { + display: flex; + + .editor-collapse { + transition: all .5s, opacity 0.3s; + position: absolute; + width: 40px; + transform-origin: top left; + } + } + + button:hover { + transform: scale(1); + } + } +} + +.webpage-urlInput { + padding: 12px 10px 11px 10px; + border: 0px; + color: grey; + letter-spacing: 2px; + outline-color: black; + background: rgb(238, 238, 238); + width: 100%; + margin-right: 10px; height: 100%; }
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 162ac1d98..9b66b2431 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,18 +1,25 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; +import { FieldResult } from "../../../new_fields/Doc"; import { HtmlField } from "../../../new_fields/HtmlField"; +import { InkTool } from "../../../new_fields/InkField"; +import { Cast, NumCast } from "../../../new_fields/Types"; import { WebField } from "../../../new_fields/URLField"; +import { Utils } from "../../../Utils"; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from './FieldView'; +import { KeyValueBox } from "./KeyValueBox"; import "./WebBox.scss"; import React = require("react"); -import { InkTool } from "../../../new_fields/InkField"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; @observer export class WebBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(WebBox); } + @observable private collapsed: boolean = true; + @observable private url: string = ""; componentWillMount() { @@ -27,6 +34,71 @@ export class WebBox extends React.Component<FieldViewProps> { this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect; } } + + this.setURL(); + } + + @action + onURLChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.url = e.target.value; + } + + @action + submitURL = () => { + const script = KeyValueBox.CompileKVPScript(`new WebField("${this.url}")`); + if (!script) return; + KeyValueBox.ApplyKVPScript(this.props.Document, "data", script); + } + + @action + setURL() { + let urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField); + if (urlField) this.url = urlField.url.toString(); + else this.url = ""; + } + + onValueKeyDown = async (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + this.submitURL(); + } + } + + urlEditor() { + return ( + <div className="webView-urlEditor" style={{ top: this.collapsed ? -70 : 0 }}> + <div className="urlEditor"> + <div className="editorBase"> + <button className="editor-collapse" + style={{ + top: this.collapsed ? 70 : 10, + transform: `rotate(${this.collapsed ? 180 : 0}deg) scale(${this.collapsed ? 0.5 : 1}) translate(${this.collapsed ? "-100%, -100%" : "0, 0"})`, + opacity: (this.collapsed && !this.props.isSelected()) ? 0 : 0.9, + left: (this.collapsed ? 0 : "unset"), + }} + title="Collapse Url Editor" onClick={this.toggleCollapse}> + <FontAwesomeIcon icon="caret-up" size="2x" /> + </button> + <div style={{ marginLeft: 54, width: "100%", display: this.collapsed ? "none" : "flex" }}> + <input className="webpage-urlInput" + placeholder="ENTER URL" + value={this.url} + onChange={this.onURLChange} + onKeyDown={this.onValueKeyDown} + /> + <button className="submitUrl" onClick={this.submitURL}> + SUBMIT URL + </button> + </div> + </div> + </div> + </div> + ); + } + + @action + toggleCollapse = () => { + this.collapsed = !this.collapsed; } _ignore = 0; @@ -52,12 +124,13 @@ export class WebBox extends React.Component<FieldViewProps> { if (field instanceof HtmlField) { view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />; + view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } else { - view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />; + view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />; } let content = <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> + {this.urlEditor()} {view} </div>; |
