diff options
Diffstat (limited to 'src/client/views')
123 files changed, 5195 insertions, 7150 deletions
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 68ccefcb5..5acb3e4c8 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -1,12 +1,14 @@ import React = require("react"); -import { observable, action } from "mobx"; +import { observable, action, runInAction } from "mobx"; import "./AntimodeMenu.scss"; +export interface AntimodeMenuProps { +} /** * This is an abstract class that serves as the base for a PDF-style or Marquee-style * menu. To use this class, look at PDFMenu.tsx or MarqueeOptionsMenu.tsx for an example. */ -export default abstract class AntimodeMenu extends React.Component { +export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Component<T, {}> { protected _offsetY: number = 0; protected _offsetX: number = 0; protected _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -18,7 +20,7 @@ export default abstract class AntimodeMenu extends React.Component { @observable protected _transitionProperty: string = "opacity"; @observable protected _transitionDuration: string = "0.5s"; @observable protected _transitionDelay: string = ""; - @observable protected _canFade: boolean = true; + @observable protected _canFade: boolean = false; @observable public Pinned: boolean = false; @@ -84,7 +86,7 @@ export default abstract class AntimodeMenu extends React.Component { @action protected togglePin = (e: React.MouseEvent) => { - this.Pinned = !this.Pinned; + runInAction(() => this.Pinned = !this.Pinned); } protected dragStart = (e: React.PointerEvent) => { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 07f7b8e6d..952100cb0 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -3,14 +3,9 @@ import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextM import { observable, action, computed, runInAction, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; import "./ContextMenu.scss"; -import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; import Measure from "react-measure"; -library.add(faSearch); -library.add(faCircle); - @observer export class ContextMenu extends React.Component { static Instance: ContextMenu; @@ -100,7 +95,7 @@ export class ContextMenu extends React.Component { } @action moveAfter(item: ContextMenuProps, after: ContextMenuProps) { - if (this.findByDescription(after.description)) { + if (after && this.findByDescription(after.description)) { const curInd = this._items.findIndex((i) => i.description === item.description); this._items.splice(curInd, 1); const afterInd = this._items.findIndex((i) => i.description === after.description); @@ -220,7 +215,7 @@ export class ContextMenu extends React.Component { @computed get menuItems() { if (!this._searchString) { - return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />); + return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} pageX={this.pageX} />); } return this.filteredViews; } diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 7e233ec04..eddecb7a7 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,12 +1,10 @@ import React = require("react"); import { observable, action } from "mobx"; import { observer } from "mobx-react"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UndoManager } from "../util/UndoManager"; - -library.add(faAngleRight); +import { NumberLiteralType } from "typescript"; export interface OriginalMenuProps { description: string; @@ -31,6 +29,7 @@ export type ContextMenuProps = OriginalMenuProps | SubmenuProps; export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> { @observable private _items: Array<ContextMenuProps> = []; @observable private overItem = false; + @observable private subRef = React.createRef<HTMLDivElement>(); constructor(props: ContextMenuProps | SubmenuProps) { super(props); @@ -54,6 +53,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select currentTimeout?: any; static readonly timeout = 300; _overPosY = 0; + _overPosX = 0; onPointerEnter = (e: React.MouseEvent) => { if (this.currentTimeout) { clearTimeout(this.currentTimeout); @@ -63,6 +63,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select return; } this._overPosY = e.clientY; + this._overPosX = e.clientX; this.currentTimeout = setTimeout(action(() => this.overItem = true), ContextMenuItem.timeout); } @@ -78,6 +79,9 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } render() { + + + if ("event" in this.props) { return ( <div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}> @@ -94,8 +98,13 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select } else if ("subitems" in this.props) { const where = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "flex-start" : this._overPosY > window.innerHeight * 2 / 3 ? "flex-end" : "center"; const marginTop = !this.overItem ? "" : this._overPosY < window.innerHeight / 3 ? "20px" : this._overPosY > window.innerHeight * 2 / 3 ? "-20px" : ""; + + // here const submenu = !this.overItem ? (null) : - <div className="contextMenu-subMenu-cont" style={{ marginLeft: "90%", left: "0px", marginTop }}> + <div className="contextMenu-subMenu-cont" + style={{ + marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? "90%" : "20%", marginTop + }}> {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)} </div>; if (!("noexpand" in this.props)) { @@ -115,7 +124,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: "center" }} > {this.props.description} - <FontAwesomeIcon icon={faAngleRight} size="lg" style={{ position: "absolute", right: "10px" }} /> + <FontAwesomeIcon icon={"angle-right"} size="lg" style={{ position: "absolute", right: "10px" }} /> </div> {submenu} </div> diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx index 9ed14509f..f4f96da8a 100644 --- a/src/client/views/DictationOverlay.tsx +++ b/src/client/views/DictationOverlay.tsx @@ -4,7 +4,7 @@ import "normalize.css"; import * as React from 'react'; import { DictationManager } from '../util/DictationManager'; import "./Main.scss"; -import MainViewModal from './MainViewModal'; +import { MainViewModal } from './MainViewModal'; @observer export class DictationOverlay extends React.Component { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 8c8bb6fde..aa888b34a 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -58,7 +58,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor: lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result; - active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.SelectedTool(); // bcz: inking state shouldn't affect static tools + active = (outsideReaction?: boolean) => !this.props.Document._isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !Doc.SelectedTool(); // bcz: inking state shouldn't affect static tools protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; @@ -131,11 +131,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey + "-annotations", doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); - doc.deleted = true; }); return true; } @@ -179,8 +178,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T added.map(doc => doc.context = this.props.Document); (targetDataDoc[this.annotationKey] as List<Doc>).push(...added); targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); } } } @@ -188,9 +185,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T } whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); - active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.props.Document._) && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0 || BoolCast((this.layoutDoc as any).forceActive)) ? true : false) - annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document.isBackground && this.props.active()) || + annotationsActive = (outsideReaction?: boolean) => (Doc.GetSelectedTool() !== InkTool.None || (this.props.Document._isBackground && this.props.active()) || (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) } return Component; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 8748b1880..cf2bd5176 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,45 +1,34 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast } from "../../fields/Doc"; +import { Doc } from "../../fields/Doc"; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast } from "../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; import { emptyFunction, setupMoveUpEvents } from "../../Utils"; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; -import { Docs, DocUtils } from '../documents/Documents'; +import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; +import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DragManager } from '../util/DragManager'; -import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; -import { ParentDocSelector } from './collections/ParentDocumentSelector'; -import './collections/ParentDocumentSelector.scss'; +import { SelectionManager } from '../util/SelectionManager'; +import { SharingManager } from '../util/SharingManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +import { TabDocView } from './collections/TabDocView'; import './DocumentButtonBar.scss'; import { MetadataEntryMenu } from './MetadataEntryMenu'; +import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; import { TemplateMenu } from "./TemplateMenu"; import { Template, Templates } from "./Templates"; import React = require("react"); -import { DocumentLinksButton } from './nodes/DocumentLinksButton'; -import { Tooltip } from '@material-ui/core'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faArrowAltCircleRight); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faPhotoVideo); - const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; @@ -164,11 +153,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } - CollectionDockingView.AddRightSplit(googleDoc); + CollectionDockingView.AddSplit(googleDoc, "right"); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); @@ -195,17 +184,57 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV @computed get pinButton() { const targetDoc = this.view0?.props.Document; - const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + let isPinned = targetDoc && Doc.isDocPinned(targetDoc); + // More than 1 document selected then all must be in presentation for isPinned to be true (then it will unpin all) + if (SelectionManager.SelectedDocuments().length > 1) { + SelectionManager.SelectedDocuments().forEach((docView: DocumentView) => { + if (Doc.isDocPinned(docView.props.Document)) isPinned = true; + else isPinned = false; + }); + } return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div></>}> <div className="documentButtonBar-linker" style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white", border: isPinned ? "black 1px solid " : "" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> + onClick={e => this.props.views().map(view => view && TabDocView.PinDoc(view.props.Document, isPinned))}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> </div></Tooltip>; } @computed + get shareButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Sharing Manager"}</div></>}> + <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="users" + /> + </div></Tooltip >; + } + + @computed + get annotateButton() { + const targetDoc = this.view0?.props.Document; + const isAnnotating = targetDoc?.isAnnotating; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`${isAnnotating ? "Exit" : "Enter"} annotation mode`}</div></>}> + <div className="documentButtonBar-linker" style={{ backgroundColor: isAnnotating ? "white" : "", color: isAnnotating ? "black" : "white", }} + onClick={e => targetDoc.isAnnotating = !targetDoc.isAnnotating}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="edit" + /> + </div></Tooltip >; + } + + @computed + get moreButton() { + const targetDoc = this.view0?.props.Document; + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Open Properties Panel"}</div></>}> + <div className="documentButtonBar-linker" style={{ color: "white" }} onClick={action(e => + CurrentUserUtils.propertiesWidth = CurrentUserUtils.propertiesWidth > 0 ? 0 : 250)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="ellipsis-h" + /> + </div></Tooltip >; + } + + @computed get metadataButton() { const view0 = this.view0; return !view0 ? (null) : <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>}> @@ -218,17 +247,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Flyout> </div></Tooltip>; } - - @computed - get contextButton() { - return !this.view0 ? (null) : <ParentDocSelector Document={this.view0.props.Document} addDocTab={(doc, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : - this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc) : - this.view0?.props.addDocTab(doc, "onRight"); - return true; - }} />; - } - @observable _aliasDown = false; onAliasButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); @@ -293,12 +311,21 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <div className="documentButtonBar-button"> {this.pinButton} </div> - {/* <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> + {!Doc.UserDoc()["documentLinksButton-hideEnd"] ? (null) : <div className="documentButtonBar-button"> + {this.shareButton} + </div>} + {![DocumentType.VID, DocumentType.WEB].includes(StrCast(this.view0.props.Document.type) as DocumentType) ? (null) : <div className="documentButtonBar-button"> + {this.annotateButton} + </div>} + <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div> <div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}> {this.considerGoogleDocsPull} - </div> */} + </div> + <div className="documentButtonBar-button"> + {this.moreButton} + </div> </div>; } } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 1e8cfdff4..e02408559 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -16,22 +16,12 @@ $linkGap : 3px; grid-template-columns: 8px 16px 1fr 8px 8px; pointer-events: none; - #documentDecorations-centerCont { + .documentDecorations-centerCont { grid-column: 3; background: none; } - .documentDecorations-resizer { - pointer-events: auto; - background: $alt-accent; - opacity: 0.1; - } - - .documentDecorations-resizer:hover { - opacity: 1; - } - - .documentDecorations-selector { + .documentDecorations-levelSelector { pointer-events: auto; height: 15px; width: 15px; @@ -49,27 +39,43 @@ $linkGap : 3px; grid-row: 4; } - #documentDecorations-topLeftResizer, - #documentDecorations-leftResizer, - #documentDecorations-bottomLeftResizer { + .documentDecorations-topLeftResizer, + .documentDecorations-topRightResizer, + .documentDecorations-bottomLeftResizer, + .documentDecorations-bottomRightResizer, + .documentDecorations-leftResizer, + .documentDecorations-topResizer, + .documentDecorations-bottomResizer, + .documentDecorations-rightResizer { + pointer-events: auto; + background: $alt-accent; + opacity: 0.1; + &:hover { + opacity: 1; + } + } + + .documentDecorations-topLeftResizer, + .documentDecorations-leftResizer, + .documentDecorations-bottomLeftResizer { grid-column: 1 } - #documentDecorations-topResizer, - #documentDecorations-bottomResizer { + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { grid-column-start: 2; grid-column-end: 5; } - #documentDecorations-bottomRightResizer, - #documentDecorations-topRightResizer, - #documentDecorations-rightResizer { + .documentDecorations-bottomRightResizer, + .documentDecorations-topRightResizer, + .documentDecorations-rightResizer { grid-column-start: 5; grid-column-end: 7; } - #documentDecorations-rotation, - #documentDecorations-borderRadius { + .documentDecorations-rotation, + .documentDecorations-borderRadius { grid-column: 5; grid-row: 4; border-radius: 100%; @@ -79,6 +85,7 @@ $linkGap : 3px; top: 12; position: relative; pointer-events: all; + cursor: grab; .borderRadiusTooltip { width: 10px; @@ -86,69 +93,69 @@ $linkGap : 3px; position: absolute; } } - #documentDecorations-rotation { + .documentDecorations-rotation { background: transparent; right: -15; } - #documentDecorations-topLeftResizer, - #documentDecorations-bottomRightResizer { + .documentDecorations-topLeftResizer, + .documentDecorations-bottomRightResizer { cursor: nwse-resize; background: unset; opacity: 1; } - #documentDecorations-topLeftResizer { + .documentDecorations-topLeftResizer { border-left: 2px solid; border-top: solid 2px; } - #documentDecorations-bottomRightResizer { + .documentDecorations-bottomRightResizer { border-right: 2px solid; border-bottom: solid 2px; } - #documentDecorations-topLeftResizer:hover, - #documentDecorations-bottomRightResizer:hover { + .documentDecorations-topLeftResizer:hover, + .documentDecorations-bottomRightResizer:hover { opacity: 1; } - #documentDecorations-bottomRightResizer { + .documentDecorations-bottomRightResizer { grid-row: 4; } - #documentDecorations-topRightResizer, - #documentDecorations-bottomLeftResizer { + .documentDecorations-topRightResizer, + .documentDecorations-bottomLeftResizer { cursor: nesw-resize; background: unset; opacity: 1; } - #documentDecorations-topRightResizer { + .documentDecorations-topRightResizer { border-right: 2px solid; border-top: 2px solid; } - #documentDecorations-bottomLeftResizer { + .documentDecorations-bottomLeftResizer { border-left: 2px solid; border-bottom: 2px solid; } - #documentDecorations-topRightResizer:hover, - #documentDecorations-bottomLeftResizer:hover { + .documentDecorations-topRightResizer:hover, + .documentDecorations-bottomLeftResizer:hover { cursor: nesw-resize; background: dimGray; opacity: 1; } - #documentDecorations-topResizer, - #documentDecorations-bottomResizer { + .documentDecorations-topResizer, + .documentDecorations-bottomResizer { cursor: ns-resize; } - #documentDecorations-leftResizer, - #documentDecorations-rightResizer { + .documentDecorations-leftResizer, + .documentDecorations-rightResizer { cursor: ew-resize; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 03746a1d2..81c1676b0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,56 +1,30 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes, faAngleLeft, faAngleRight, faAngleDoubleLeft, faAngleDoubleRight, faPause, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, reaction, runInAction, get } from "mobx"; +import { Tooltip } from '@material-ui/core'; +import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DataSym, Field, WidthSym, HeightSym, AclEdit, AclAdmin } from "../../fields/Doc"; +import { AclAdmin, AclEdit, DataSym, Doc, Field, WidthSym, HeightSym } from "../../fields/Doc"; import { Document } from '../../fields/documentSchemas'; +import { HtmlField } from '../../fields/HtmlField'; +import { InkField } from "../../fields/InkField"; import { ScriptField } from '../../fields/ScriptField'; -import { Cast, StrCast, NumCast } from "../../fields/Types"; -import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils"; -import { DocUtils } from "../documents/Documents"; +import { Cast, NumCast } from "../../fields/Types"; +import { GetEffectiveAcl } from '../../fields/util'; +import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from "../../Utils"; +import { DocUtils, Docs } from "../documents/Documents"; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; +import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from "../util/UndoManager"; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { DocumentView } from "./nodes/DocumentView"; import React = require("react"); import e = require('express'); -import { CollectionDockingView } from './collections/CollectionDockingView'; -import { SnappingManager } from '../util/SnappingManager'; -import { HtmlField } from '../../fields/HtmlField'; -import { InkField } from "../../fields/InkField"; -import { Tooltip } from '@material-ui/core'; -import { GetEffectiveAcl } from '../../fields/util'; -import { DocumentIcon } from './nodes/DocumentIcon'; -import { render } from 'react-dom'; -import { createLessThan } from 'typescript'; -import FormatShapePane from './collections/collectionFreeForm/FormatShapePane'; -import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; - -library.add(faCaretUp); -library.add(faObjectGroup); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm, faTextHeight); -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faAngleDoubleLeft); -library.add(faAngleDoubleRight); -library.add(faAngleLeft); -library.add(faAngleRight); -library.add(faPause); -library.add(faExternalLinkAlt); +import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { InkStrokeProperties } from './InkStrokeProperties'; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -121,7 +95,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith("#")) { const selectionTitleFieldKey = this._titleControlString.substring(1); - selectionTitleFieldKey === "title" && (SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._accumulatedTitle.startsWith("-")); + selectionTitleFieldKey === "title" && (SelectionManager.SelectedDocuments()[0].dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-")); UndoManager.RunInBatch(() => selectionTitleFieldKey && SelectionManager.SelectedDocuments().forEach(d => { const value = typeof d.props.Document[selectionTitleFieldKey] === "number" ? +this._accumulatedTitle : this._accumulatedTitle; Doc.SetInPlace(d.props.Document, selectionTitleFieldKey, value, true); @@ -212,8 +186,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (e.button === 0) { const selectedDocs = SelectionManager.SelectedDocuments(); if (selectedDocs.length) { - //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0], selectedDocs[0].props.LibraryPath); - CollectionDockingView.AddRightSplit(Doc.MakeAlias(selectedDocs[0].props.Document), selectedDocs[0].props.LibraryPath); + if (e.ctrlKey) { + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0]); + CollectionDockingView.AddSplit(alias, "right"); + } else if (e.shiftKey) { + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + alias.x = -alias[WidthSym]() / 2; + alias.y = -alias[HeightSym]() / 2; + //CollectionDockingView.Instance?.OpenFullScreen(selectedDocs[0]); + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); + } else { + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); + } } } SelectionManager.DeselectAll(); @@ -310,7 +297,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const newY = Math.sin(angle) * (ink.X - this._centerPoints[index].X) + Math.cos(angle) * (ink.Y - this._centerPoints[index].Y) + this._centerPoints[index].Y; newPoints.push({ X: newX, Y: newY }); } - doc.data = new InkField(newPoints); + Doc.GetProto(doc).data = new InkField(newPoints); const xs = newPoints.map(p => p.X); const ys = newPoints.map(p => p.Y); const left = Math.min(...xs); @@ -349,7 +336,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const doc = Document(element.rootDoc); if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height) { this._inkDocs.push({ x: doc.x, y: doc.y, width: doc._width, height: doc._height }); - if (FormatShapePane.Instance._lock) { + if (InkStrokeProperties.Instance?._lock) { doc._nativeHeight = doc._height; doc._nativeWidth = doc._width; } @@ -358,13 +345,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { }); if (e.button === 0) { - this._resizeHdlId = e.currentTarget.id; + this._resizeHdlId = e.currentTarget.className; const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; this.Interacting = true; this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); - SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(); + SelectionManager.SelectedDocuments()[0].props.setupDragLines?.(e.ctrlKey || e.shiftKey); } this._snapX = e.pageX; this._snapY = e.pageY; @@ -382,7 +369,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> var fixedAspect = first.layoutDoc._nativeWidth ? NumCast(first.layoutDoc._nativeWidth) / NumCast(first.layoutDoc._nativeHeight) : 0; SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { const doc = Document(element.rootDoc); - if (doc.type === DocumentType.INK && doc._width && doc._height && FormatShapePane.Instance._lock) { + if (doc.type === DocumentType.INK && doc._width && doc._height && InkStrokeProperties.Instance?._lock) { fixedAspect = NumCast(doc._nativeWidth) / NumCast(doc._nativeHeight); } })); @@ -492,10 +479,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> doc[DataSym][fieldKey + "-nativeHeight"] = doc._nativeHeight = nheight = doc._height || 0; } const anno = Cast(doc.annotationOn, Doc, null); - if (e.ctrlKey && anno) { + if (e.ctrlKey && (anno || doc.type === DocumentType.IMG)) { dW !== 0 && runInAction(() => { - const dataDoc = anno[DataSym]; - const annoFieldKey = Doc.LayoutFieldKey(anno); + const dataDoc = (anno ?? doc)[DataSym]; + const annoFieldKey = Doc.LayoutFieldKey(anno ?? doc); const nw = NumCast(dataDoc[annoFieldKey + "-nativeWidth"]); const nh = NumCast(dataDoc[annoFieldKey + "-nativeHeight"]); dataDoc[annoFieldKey + "-nativeWidth"] = nw + (dW > 0 ? 10 : -10); @@ -558,7 +545,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> const newY = ((doc.y || 0) - this._inkDocs[index].y) + (i.Y * (doc._height || 0)) / this._inkDocs[index].height; newPoints.push({ X: newX, Y: newY }); }); - doc.data = new InkField(newPoints); + Doc.GetProto(doc).data = new InkField(newPoints); } doc._nativeWidth = 0; @@ -591,19 +578,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } public static DocumentIcon(layout: string) { - const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : - layout.indexOf("ImageBox") !== -1 ? faImage : - layout.indexOf("Formatted") !== -1 ? faStickyNote : - layout.indexOf("Video") !== -1 ? faFilm : - layout.indexOf("Collection") !== -1 ? faObjectGroup : - faCaretUp; + const button = layout.indexOf("PDFBox") !== -1 ? "file-pdf" : + layout.indexOf("ImageBox") !== -1 ? "image" : + layout.indexOf("Formatted") !== -1 ? "sticky-note" : + layout.indexOf("Video") !== -1 ? "film" : + layout.indexOf("Collection") !== -1 ? "object-group" : + "caret-up"; return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } render() { - const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined; + const darkScheme = CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimgray" : undefined; const bounds = this.Bounds; const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } const canDelete = SelectionManager.SelectedDocuments().some(docView => { @@ -611,39 +598,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return collectionAcl === AclAdmin || collectionAcl === AclEdit; }); const minimal = bounds.r - bounds.x < 100 ? true : false; - const maximizeIcon = minimal ? ( - <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"> - <div className="documentDecorations-contextMenu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> - </div></Tooltip>) : canDelete ? ( - <Tooltip title={<><div className="dash-tooltip">Close</div></>} placement="top"> - <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> - {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/} - <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" /> - </div></Tooltip>) : (null); + const closeIcon = canDelete ? ( + <Tooltip title={<div className="dash-tooltip">Close</div>} placement="top"> + <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> + <FontAwesomeIcon className="documentdecorations-times" icon={"times"} size="lg" /> + </div></Tooltip>) : (null); const titleArea = this._edtingTitle ? + <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} + onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> : <> - <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} - onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} /> - {minimal ? (null) : <div className="publishBox" // title="make document referenceable by its title" - // onPointerDown={action(e => { - // if (!seldoc.props.Document.customTitle) { - // seldoc.props.Document.customTitle = true; - // StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1)); - // this._accumulatedTitle = StrCast(seldoc.props.Document.title); - // } - // DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument); - // })} - > - {/* <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon> */} - </div>} - </> : - <> - {minimal ? (null) : <Tooltip title={<><div className="dash-tooltip">Show context menu</div></>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> - <FontAwesomeIcon size="lg" icon="cog" /> + {minimal ? (null) : <Tooltip title={<div className="dash-tooltip">Show context menu</div>} placement="top"><div className="documentDecorations-contextMenu" key="menu" onPointerDown={this.onSettingsDown}> + <FontAwesomeIcon size="lg" icon="bars" /> </div></Tooltip>} - <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > + <div className="documentDecorations-title" style={{ gridColumnEnd: minimal ? 4 : 5, gridColumnStart: minimal ? 2 : 3 }} key="title" onPointerDown={this.onTitleDown} > <span style={{ width: "100%", display: "inline-block", cursor: "move" }}>{`${this.selectionTitle}`}</span> </div> </>; @@ -659,8 +627,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (bounds.y > bounds.b) { bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight); } - var offset = 0; - let useRotation = seldoc.rootDoc.type === DocumentType.INK; + const useRotation = seldoc.rootDoc.type === DocumentType.INK; return (<div className="documentDecorations" style={{ background: darkScheme }} > <div className="documentDecorations-background" style={{ @@ -674,45 +641,37 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> </div> {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> <div className="documentDecorations-container" key="container" ref={this.setTextBar} style={{ - width: (bounds.r - bounds.x + this._resizeBorderWidth + offset) + "px", - height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + offset) + "px", - left: bounds.x - this._resizeBorderWidth / 2 - offset / 2, - top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight - offset / 2, + width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", + height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px", + left: bounds.x - this._resizeBorderWidth / 2, + top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> - {maximizeIcon} + {closeIcon} {titleArea} - {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK ? (null) : - <Tooltip title={<><div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div></>} placement="top"> + {SelectionManager.SelectedDocuments().length !== 1 || seldoc.Document.type === DocumentType.INK || minimal ? (null) : + <Tooltip title={<div className="dash-tooltip">{`${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`}</div>} placement="top"> <div className="documentDecorations-iconifyButton" onPointerDown={this.onIconifyDown}> <FontAwesomeIcon icon={seldoc.finalLayoutKey.includes("icon") ? "window-restore" : "window-minimize"} className="documentView-minimizedIcon" /> </div></Tooltip>} - <Tooltip title={<><div className="dash-tooltip">Open In a New Pane</div></>} placement="top"><div className="documentDecorations-openInTab" onPointerDown={this.onMaximizeDown}> + <Tooltip title={<div className="dash-tooltip">Open In a New Pane</div>} placement="top"><div className="documentDecorations-openInTab" onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} onPointerDown={this.onMaximizeDown}> {SelectionManager.SelectedDocuments().length === 1 ? <FontAwesomeIcon icon="external-link-alt" className="documentView-minimizedIcon" /> : "..."} </div></Tooltip> - <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-topResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-centerCont"></div> - <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> + <div className="documentDecorations-topLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-topResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-topRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-leftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-centerCont"></div> + <div className="documentDecorations-rightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomLeftResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> + <div className="documentDecorations-bottomRightResizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - <Tooltip title={<><div className="dash-tooltip">tap to select containing document</div></>} placement="top"> - <div id="documentDecorations-levelSelector" className="documentDecorations-selector" + <Tooltip title={<div className="dash-tooltip">tap to select containing document</div>} placement="top"> + <div className="documentDecorations-levelSelector" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}> - <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" /> + <FontAwesomeIcon className="documentdecorations-times" icon={"arrow-alt-circle-up"} size="lg" /> </div></Tooltip>} - <div id={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} + <div className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`} onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}>{useRotation && "⟲"}</div> </div > diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index ec3e754fb..1b4b9a2be 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -4,10 +4,8 @@ import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; -import "./EditableView.scss"; import { DragManager } from '../util/DragManager'; -import { ComputedField } from '../../fields/ScriptField'; -import { FieldValue } from '../../fields/Types'; +import "./EditableView.scss"; export interface EditableProps { /** @@ -54,10 +52,6 @@ export interface EditableProps { color?: string | undefined; onDrop?: any; placeholder?: string; - highlight?: boolean; - positions?: number[]; - search?: string; - bing?: () => string | undefined; } /** @@ -96,25 +90,30 @@ export class EditableView extends React.Component<EditableProps> { @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Tab") { - e.stopPropagation(); - this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); - this.props.OnTab && this.props.OnTab(e.shiftKey); - } else if (e.key === "Enter") { - e.stopPropagation(); - if (!e.ctrlKey) { + switch (e.key) { + case "Tab": + e.stopPropagation(); this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); - } else if (this.props.OnFillDown) { - this.props.OnFillDown(e.currentTarget.value); + this.props.OnTab && this.props.OnTab(e.shiftKey); + break; + case "Enter": + e.stopPropagation(); + if (!e.ctrlKey) { + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false); + } else if (this.props.OnFillDown) { + this.props.OnFillDown(e.currentTarget.value); + this._editing = false; + this.props.isEditingCallback?.(false); + } + break; + case "Escape": + e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); - } - } else if (e.key === "Escape") { - e.stopPropagation(); - this._editing = false; - this.props.isEditingCallback?.(false); - } else if (e.key === ":") { - this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; + case ":": + this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; } } @@ -123,11 +122,9 @@ export class EditableView extends React.Component<EditableProps> { e.nativeEvent.stopPropagation(); if (this._ref.current && this.props.showMenuOnLoad) { this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); - } else { - if (!this.props.onClick?.(e)) { - this._editing = true; - this.props.isEditingCallback?.(true); - } + } else if (!this.props.onClick?.(e)) { + this._editing = true; + this.props.isEditingCallback?.(true); } e.stopPropagation(); } @@ -154,7 +151,7 @@ export class EditableView extends React.Component<EditableProps> { @action setIsFocused = (value: boolean) => { const wasFocused = this._editing; - //this._editing = value; + this._editing = value; return wasFocused !== this._editing; } @@ -171,6 +168,7 @@ export class EditableView extends React.Component<EditableProps> { onPointerDown: this.stopPropagation, onClick: this.stopPropagation, onPointerUp: this.stopPropagation, + onKeyPress: this.stopPropagation, value: this.props.autosuggestProps.value, onChange: this.props.autosuggestProps.onChange }} @@ -179,6 +177,7 @@ export class EditableView extends React.Component<EditableProps> { defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus={true} + onKeyPress={e => e.stopPropagation()} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20 }} @@ -186,51 +185,25 @@ export class EditableView extends React.Component<EditableProps> { />; } - returnHighlights() { - const results = []; - const contents = this.props.bing!(); - - if (contents !== undefined) { - if (this.props.positions !== undefined) { - const positions = this.props.positions; - const length = this.props.search!.length; - - // contents = String(this.props.contents.valueOf()); - - results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(0, this.props.positions[0]) : this.props.placeholder?.valueOf()}</span>); - positions.forEach((num, cur) => { - results.push(<span style={{ backgroundColor: "#FFFF00", fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num, num + length) : this.props.placeholder?.valueOf()}</span>); - let end = 0; - cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1]; - results.push(<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{contents ? contents.slice(num + length, end) : this.props.placeholder?.valueOf()}</span>); - } - ); - } - return results; - } - else { - return <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>; - } - } - render() { if (this._editing && this.props.GetValue() !== undefined) { return this.props.sizeToContent ? <div style={{ display: "grid", minWidth: 100 }}> - <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}>{this.props.GetValue()}</div> + <div style={{ display: "inline-block", position: "relative", height: 0, width: "100%", overflow: "hidden" }}> + {this.props.GetValue()} + </div> {this.renderEditor()} - </div> : this.renderEditor(); - } else { - setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); - return (this.props.contents instanceof ObjectField ? (null) : - <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} - ref={this._ref} - style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} - onClick={this.onClick} placeholder={this.props.placeholder}> - {this.props.highlight === undefined || this.props.positions === undefined || this.props.bing === undefined ? <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize, color: this.props.contents ? "black" : "grey" }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span> - : this.returnHighlights()} - </div> - ); + </div> : + this.renderEditor(); } + setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); + return this.props.contents instanceof ObjectField ? (null) : + <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} + style={{ display: this.props.display, minHeight: "17px", whiteSpace: "nowrap", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }} + onClick={this.onClick} placeholder={this.props.placeholder}> + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{ + this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} + </span> + </div>; } }
\ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 76e786257..63711a3cb 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -7,7 +7,7 @@ import { Cast, FieldValue, NumCast } from "../../fields/Types"; import MobileInkOverlay from "../../mobile/MobileInkOverlay"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; import { MobileInkOverlayContent } from "../../server/Message"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter, setupMoveUpEvents, returnEmptyDoclist } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocUtils } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; @@ -26,7 +26,7 @@ import * as fitCurve from 'fit-curve'; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; @observer -export default class GestureOverlay extends Touchable { +export class GestureOverlay extends Touchable { static Instance: GestureOverlay; @observable public InkShape: string = ""; @@ -613,10 +613,10 @@ export default class GestureOverlay extends Touchable { } } // if we're not drawing in a toolglass try to recognize as gesture - else { + else { // need to decide when to turn gestures back on const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points)); let actionPerformed = false; - if (result && result.Score > 0.7) { + if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case GestureUtils.Gestures.Box: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Box); break; case GestureUtils.Gestures.StartBracket: actionPerformed = this.dispatchGesture(GestureUtils.Gestures.StartBracket); break; @@ -754,20 +754,64 @@ export default class GestureOverlay extends Touchable { break; case "circle": + // const centerX = (right + left) / 2; + // const centerY = (bottom + top) / 2; + // const radius = bottom - centerY; + + + // for (var y = top; y < bottom; y++) { + // const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + // this._points.push({ X: x, Y: y }); + // } + // for (var y = bottom; y > top; y--) { + // const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + // const newX = centerX - (x - centerX); + // this._points.push({ X: newX, Y: y }); + // } + // this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); + // this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 }); + const centerX = (right + left) / 2; const centerY = (bottom + top) / 2; - const radius = bottom - centerY; - for (var y = top; y < bottom; y++) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - this._points.push({ X: x, Y: y }); - } - for (var y = bottom; y > top; y--) { - const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; - const newX = centerX - (x - centerX); - this._points.push({ X: newX, Y: y }); + if ((bottom - centerY) < (right - centerX)) { + const radius = bottom - centerY; + for (var y = top; y < bottom; y++) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + this._points.push({ X: x, Y: y }); + } + for (var y = bottom; y > top; y--) { + const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX; + const newX = centerX - (x - centerX); + this._points.push({ X: newX, Y: y }); + } + this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); + this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 }); + + } else { + //right = bottom + //left = top + const radius = right - centerX; + for (var x = left; x < right; x++) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + this._points.push({ X: x, Y: y }); + } + for (var x = right; x > left; x--) { + const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY; + const newY = centerY - (y - centerY); + this._points.push({ X: x, Y: newY }); + } + this._points.push({ X: left, Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((left - centerX), 2))) + centerY }); + this._points.push({ X: left, Y: (Math.sqrt(Math.pow(radius, 2) - (Math.pow((left - centerX), 2))) + centerY) - 1 }); + + } - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top }); - this._points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((top - centerY), 2))) + centerX, Y: top - 1 }); + + + + + + + break; case "line": @@ -891,6 +935,7 @@ export default class GestureOverlay extends Touchable { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} />; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index be6aa6be2..83c02b09b 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -6,23 +6,26 @@ import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { ScriptField } from "../../fields/ScriptField"; import { Cast, PromiseValue } from "../../fields/Types"; -import GoogleAuthenticationManager from "../apis/GoogleAuthenticationManager"; +import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager"; import { DocServer } from "../DocServer"; import { DocumentType } from "../documents/DocumentTypes"; import { DictationManager } from "../util/DictationManager"; import { DragManager } from "../util/DragManager"; +import { GroupManager } from "../util/GroupManager"; import { SelectionManager } from "../util/SelectionManager"; -import SharingManager from "../util/SharingManager"; +import { SharingManager } from "../util/SharingManager"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; +import { ContextMenu } from "./ContextMenu"; import { DocumentDecorations } from "./DocumentDecorations"; +import { InkStrokeProperties } from "./InkStrokeProperties"; import { MainView } from "./MainView"; -import { DocumentView } from "./nodes/DocumentView"; import { DocumentLinksButton } from "./nodes/DocumentLinksButton"; -import PDFMenu from "./pdf/PDFMenu"; -import { ContextMenu } from "./ContextMenu"; -import GroupManager from "../util/GroupManager"; -import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; +import { DocumentView } from "./nodes/DocumentView"; +import { PDFMenu } from "./pdf/PDFMenu"; +import { SnappingManager } from "../util/SnappingManager"; +import { SearchBox } from "./search/SearchBox"; const modifiers = ["control", "meta", "shift", "alt"]; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>; @@ -31,7 +34,7 @@ type KeyControlInfo = { stopPropagation: boolean }; -export default class KeyManager { +export class KeyManager { public static Instance: KeyManager = new KeyManager(); private router = new Map<string, KeyHandler>(); @@ -81,26 +84,18 @@ export default class KeyManager { // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case "escape": - // if (DocumentLinksButton.StartLink) { - // if (DocumentLinksButton.StartLink.Document) { - // action((e: React.PointerEvent<HTMLDivElement>) => { - // Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - // }); - // } - // } DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = false); - const main = MainView.Instance; Doc.SetSelectedTool(InkTool.None); var doDeselect = true; - if (main.isPointerDown) { + if (SnappingManager.GetIsDragging()) { DragManager.AbortDrag(); + } else if (CollectionDockingView.Instance.HasFullScreen) { + CollectionDockingView.Instance.CloseFullScreen(); } else { - if (CollectionDockingView.Instance.HasFullScreen()) { - CollectionDockingView.Instance.CloseFullScreen(); - } else { - doDeselect = !ContextMenu.Instance.closeMenu(); - } + doDeselect = !ContextMenu.Instance.closeMenu(); } doDeselect && SelectionManager.DeselectAll(); DictationManager.Controls.stop(); @@ -113,30 +108,18 @@ export default class KeyManager { break; case "delete": case "backspace": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } const selected = SelectionManager.SelectedDocuments().slice(); - UndoManager.RunInBatch(() => { - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - }, "delete"); + UndoManager.RunInBatch(() => selected.map(dv => dv.props.removeDocument?.(dv.props.Document)), "delete"); SelectionManager.DeselectAll(); break; - case "arrowleft": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); - break; - case "arrowright": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(1, 0)), "nudge right"); - break; - case "arrowup": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -1)), "nudge up"); - break; - case "arrowdown": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 1)), "nudge down"); - break; + case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-1, 0)), "nudge left"); break; + case "arrowright": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(1, 0)), "nudge right"); break; + case "arrowup": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -1)), "nudge up"); break; + case "arrowdown": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 1)), "nudge down"); break; } return { @@ -150,22 +133,10 @@ export default class KeyManager { const preventDefault = false; switch (keyname) { - // case "~": - // DictationManager.Controls.listen({ useOverlay: true, tryExecute: true }); - // stopPropagation = true; - // preventDefault = true; - case "arrowleft": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-10, 0)), "nudge left"); - break; - case "arrowright": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(10, 0)), "nudge right"); - break; - case "arrowup": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -10)), "nudge up"); - break; - case "arrowdown": - UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 10)), "nudge down"); - break; + case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(-10, 0)), "nudge left"); break; + case "arrowright": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(10, 0)), "nudge right"); break; + case "arrowup": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, -10)), "nudge up"); break; + case "arrowdown": UndoManager.RunInBatch(() => SelectionManager.SelectedDocuments().map(dv => dv.props.nudge?.(0, 10)), "nudge down"); break; } return { @@ -204,55 +175,32 @@ export default class KeyManager { switch (keyname) { case "arrowright": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } - MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform); + MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, "right"); break; case "arrowleft": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } - MainView.Instance.mainFreeform && CollectionDockingView.CloseRightSplit(MainView.Instance.mainFreeform); + MainView.Instance.mainFreeform && CollectionDockingView.CloseSplit(MainView.Instance.mainFreeform); break; case "backspace": - if (document.activeElement) { - if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; - } + if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { + return { stopPropagation: false, preventDefault: false }; } break; case "t": PromiseValue(Cast(Doc.UserDoc()["tabs-button-tools"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 240) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 240; - } - break; - case "l": - PromiseValue(Cast(Doc.UserDoc()["tabs-button-library"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 250) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 250; - } break; case "f": - PromiseValue(Cast(Doc.UserDoc()["tabs-button-search"], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); - if (MainView.Instance.flyoutWidth === 400) { - MainView.Instance.flyoutWidth = 0; - } else { - MainView.Instance.flyoutWidth = 400; - } + SearchBox.Instance._searchFullDB = "My Stuff"; + SearchBox.Instance.enter(undefined); break; case "o": const target = SelectionManager.SelectedDocuments()[0]; - target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target); + target && CollectionDockingView.OpenFullScreen(target.props.Document); break; case "r": preventDefault = false; @@ -323,8 +271,6 @@ export default class KeyManager { undoBatch(() => { targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]); targetDataDoc[fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); })(); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts new file mode 100644 index 000000000..ad5c70fb1 --- /dev/null +++ b/src/client/views/InkStrokeProperties.ts @@ -0,0 +1,268 @@ +import { action, computed, observable } from "mobx"; +import { ColorState } from 'react-color'; +import { Doc, Field, Opt } from "../../fields/Doc"; +import { Document } from "../../fields/documentSchemas"; +import { InkField } from "../../fields/InkField"; +import { Cast, NumCast } from "../../fields/Types"; +import { DocumentType } from "../documents/DocumentTypes"; +import { SelectionManager } from "../util/SelectionManager"; +import { undoBatch } from "../util/UndoManager"; + +export class InkStrokeProperties { + static Instance: InkStrokeProperties | undefined; + + private _lastFill = "#D0021B"; + private _lastLine = "#D0021B"; + private _lastDash = "2"; + + @observable _lock = false; + @observable _controlBtn = false; + @observable _currPoint = -1; + + getField(key: string) { + return this.selectedInk?.reduce((p, i) => + (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); + } + + @computed get selectedInk() { + const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); + return inks.length ? inks : undefined; + } + @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } + @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } + @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } + @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } + @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } + @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } + @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } + @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + @computed get markHead() { return this.getField("strokeStartMarker") || ""; } + @computed get markTail() { return this.getField("strokeEndMarker") || ""; } + @computed get shapeHgt() { return this.getField("_height"); } + @computed get shapeWid() { return this.getField("_width"); } + @computed get shapeXps() { return this.getField("x"); } + @computed get shapeYps() { return this.getField("y"); } + @computed get shapeRot() { return this.getField("rotation"); } + set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } + set solidFil(value) { this.unFilled = !value; } + set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } + set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } + set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } + set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } + set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } + set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } + set dashdStk(value) { + value && (this._lastDash = value) && (this.unStrokd = false); + this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); + } + set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } + set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } + set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } + set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } + set shapeWid(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldWidth = NumCast(i.rootDoc._width); + i.rootDoc._width = Number(value); + this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); + }); + } + set shapeHgt(value) { + this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldHeight = NumCast(i.rootDoc._height); + i.rootDoc._height = Number(value); + this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); + }); + } + + constructor() { + InkStrokeProperties.Instance = this; + } + + @undoBatch + @action + addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const newPoints: { X: number, Y: number }[] = []; + var counter = 0; + for (var k = 0; k < index; k++) { + control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); + } + //decide where to put the new coordinate + const spNum = Math.floor(counter / 2) * 4 + 2; + + for (var i = 0; i < spNum; i++) { + ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + for (var j = 0; j < 4; j++) { + newPoints.push({ X: x, Y: y }); + + } + for (var i = spNum; i < ink.length; i++) { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + this._currPoint = -1; + Doc.GetProto(doc).data = new InkField(newPoints); + } + } + } + })); + } + + @undoBatch + @action + deletePoints = () => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1 && this._currPoint !== -1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink && ink.length > 4) { + const newPoints: { X: number, Y: number }[] = []; + const toRemove = Math.floor(((this._currPoint + 2) / 4)); + for (var i = 0; i < ink.length; i++) { + if (Math.floor((i + 2) / 4) !== toRemove) { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + this._currPoint = -1; + Doc.GetProto(doc).data = new InkField(newPoints); + if (newPoints.length === 4) { + const newerPoints: { X: number, Y: number }[] = []; + newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); + newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); + newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); + newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); + Doc.GetProto(doc).data = new InkField(newerPoints); + + } + } + } + } + })); + } + + @undoBatch + @action + rotate = (angle: number) => { + const _centerPoints: { X: number, Y: number }[] = []; + SelectionManager.SelectedDocuments().forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + _centerPoints.push({ X: left, Y: top }); + } + } + })); + + var index = 0; + SelectionManager.SelectedDocuments().forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + doc.rotation = Number(doc.rotation) + Number(angle); + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + ink.forEach(i => { + const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; + newPoints.push({ X: newX, Y: newY }); + }); + Doc.GetProto(doc).data = new InkField(newPoints); + const xs = newPoints.map(p => p.X); + const ys = newPoints.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + const right = Math.max(...xs); + const bottom = Math.max(...ys); + + doc._height = (bottom - top); + doc._width = (right - left); + } + index++; + } + })); + } + + @undoBatch + @action + control = (xDiff: number, yDiff: number, controlNum: number) => { + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + + const newPoints: { X: number, Y: number }[] = []; + const order = controlNum % 4; + for (var i = 0; i < ink.length; i++) { + if (controlNum === i || + (order === 0 && i === controlNum + 1) || + (order === 0 && controlNum !== 0 && i === controlNum - 2) || + (order === 0 && controlNum !== 0 && i === controlNum - 1) || + (order === 3 && i === controlNum - 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || + (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) + || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) + ) { + newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); + } + else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + const oldx = doc.x; + const oldy = doc.y; + const xs = ink.map(p => p.X); + const ys = ink.map(p => p.Y); + const left = Math.min(...xs); + const top = Math.min(...ys); + Doc.GetProto(doc).data = new InkField(newPoints); + const xs2 = newPoints.map(p => p.X); + const ys2 = newPoints.map(p => p.Y); + const left2 = Math.min(...xs2); + const top2 = Math.min(...ys2); + const right2 = Math.max(...xs2); + const bottom2 = Math.max(...ys2); + doc._height = (bottom2 - top2); + doc._width = (right2 - left2); + //if points move out of bounds + + doc.x = oldx - (left - left2); + doc.y = oldy - (top - top2); + + } + } + } + })); + } + + @undoBatch + @action + switchStk = (color: ColorState) => { + const val = String(color.hex); + this.colorStk = val; + return true; + } + + @undoBatch + @action + switchFil = (color: ColorState) => { + const val = String(color.hex); + this.colorFil = val; + return true; + } +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 41311ed86..da98eca73 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,27 +1,22 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faPaintBrush } from "@fortawesome/free-solid-svg-icons"; +import { action } from "mobx"; import { observer } from "mobx-react"; +import { Doc } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; +import { setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { InteractionUtils } from "../util/InteractionUtils"; +import { Scripting } from "../util/Scripting"; +import { UndoManager } from "../util/UndoManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import "./InkingStroke.scss"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); -import { Scripting } from "../util/Scripting"; -import { Doc } from "../../fields/Doc"; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import { action } from "mobx"; -import { setupMoveUpEvents } from "../../Utils"; -import { undoBatch, UndoManager } from "../util/UndoManager"; - - -library.add(faPaintBrush); +import { InkStrokeProperties } from "./InkStrokeProperties"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -47,11 +42,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this.props.Document.isInkMask = true; } - @action - private formatShape = () => { - FormatShapePane.Instance.Pinned = true; - } - public _prevX = 0; public _prevY = 0; private _controlNum = 0; @@ -66,15 +56,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume @action changeCurrPoint = (i: number) => { - FormatShapePane.Instance._currPoint = i; + if (!InkStrokeProperties.Instance) return; + InkStrokeProperties.Instance._currPoint = i; document.addEventListener("keydown", this.delPts, true); } @action onControlMove = (e: PointerEvent, down: number[]): boolean => { + if (!InkStrokeProperties.Instance) return false; const xDiff = this._prevX - e.clientX; const yDiff = this._prevY - e.clientY; - FormatShapePane.Instance.control(xDiff, yDiff, this._controlNum); + InkStrokeProperties.Instance.control(xDiff, yDiff, this._controlNum); this._prevX = e.clientX; this._prevY = e.clientY; return false; @@ -89,8 +81,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } @action delPts = (e: KeyboardEvent | React.PointerEvent | undefined) => { - if (e instanceof KeyboardEvent ? e.key === "-" : true) { - FormatShapePane.Instance.deletePoints(); + if (InkStrokeProperties.Instance && (e instanceof KeyboardEvent ? e.key === "-" : true)) { + InkStrokeProperties.Instance.deletePoints(); } } @@ -98,34 +90,40 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume public static MaskDim = 50000; render() { TraceMobx(); + const formatInstance = InkStrokeProperties.Instance; + if (!formatInstance) return (null); const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; // const strokeWidth = Number(StrCast(this.layoutDoc.strokeWidth, ActiveInkWidth())); const strokeWidth = Number(this.layoutDoc.strokeWidth); const xs = data.map(p => p.X); const ys = data.map(p => p.Y); - const left = Math.min(...xs) - strokeWidth / 2; - const top = Math.min(...ys) - strokeWidth / 2; - const right = Math.max(...xs) + strokeWidth / 2; - const bottom = Math.max(...ys) + strokeWidth / 2; - const width = Math.max(right - left); + const lineTop = Math.min(...ys); + const lineBot = Math.max(...ys); + const lineLft = Math.min(...xs); + const lineRgt = Math.max(...xs); + const left = lineLft - strokeWidth / 2; + const top = lineTop - strokeWidth / 2; + const right = lineRgt + strokeWidth / 2; + const bottom = lineBot + strokeWidth / 2; + const width = Math.max(1, right - left); const height = Math.max(1, bottom - top); const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); const strokeColor = StrCast(this.layoutDoc.color, ""); const points = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); const hpoints = InteractionUtils.CreatePolyline(data, left, top, this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, (strokeWidth + 15), - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), - "none", "none", "0", scaleX, scaleY, "", this.props.active() ? "visiblepainted" : "none", false, true); + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + "none", "none", undefined, scaleX, scaleY, "", "visiblepainted", false, true); //points for adding const apoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "transparent"), + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); @@ -161,36 +159,31 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const dotsize = String(Math.max(width * scaleX, height * scaleY) / 40); const addpoints = apoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="invisible" stroke-width={String(Number(dotsize) / 2)} fill="invisible" - onPointerDown={(e) => { FormatShapePane.Instance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" + <svg height="10" width="10" key={`add${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="invisible" strokeWidth={String(Number(dotsize) / 2)} fill="invisible" + onPointerDown={(e) => { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> </svg>); const controls = controlPoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="red" + <svg height="10" width="10" key={`ctrl${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" strokeWidth={String(Number(dotsize) / 2)} fill="red" onPointerDown={(e) => { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="all-scroll" /> </svg>); const handles = handlePoints.map((pts, i) => - - <svg height="10" width="10"> - <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" stroke-width={String(Number(dotsize) / 2)} fill="green" - onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="all-scroll" display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> + <svg height="10" width="10" key={`hdl${i}`}> + <circle cx={(pts.X - left - strokeWidth / 2) * scaleX + strokeWidth / 2} cy={(pts.Y - top - strokeWidth / 2) * scaleY + strokeWidth / 2} r={dotsize} stroke="black" strokeWidth={String(Number(dotsize) / 2)} fill="green" + onPointerDown={(e) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="all-scroll" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> </svg>); const handleLines = handleLine.map((pts, i) => - - <svg height="100" width="100"> + <svg height="100" width="100" key={`line${i}`}> <line x1={(pts.X1 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y1 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)} - display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> + x2={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={String(Number(dotsize) / 2)} + display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> <line x1={(pts.X2 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y1={(pts.Y2 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} - x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" stroke-width={String(Number(dotsize) / 2)} - display={(pts.dot1 === FormatShapePane.Instance._currPoint || pts.dot2 === FormatShapePane.Instance._currPoint) ? "inherit" : "none"} /> - + x2={(pts.X3 - left - strokeWidth / 2) * scaleX + strokeWidth / 2} y2={(pts.Y3 - top - strokeWidth / 2) * scaleY + strokeWidth / 2} stroke="green" strokeWidth={String(Number(dotsize) / 2)} + display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> </svg>); @@ -209,6 +202,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume if (cm) { !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); cm.addItem({ description: "Make Mask", event: this.makeMask, icon: "paint-brush" }); + cm.addItem({ description: "Edit Points", event: action(() => formatInstance._controlBtn = !formatInstance._controlBtn), icon: "paint-brush" }); //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); } }} @@ -216,10 +210,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume </defs> {hpoints} {points} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? addpoints : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? controls : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handles : ""} - {FormatShapePane.Instance._controlBtn && this.props.isSelected() ? handleLines : ""} + {formatInstance._controlBtn && this.props.isSelected() ? addpoints : ""} + {formatInstance._controlBtn && this.props.isSelected() ? controls : ""} + {formatInstance._controlBtn && this.props.isSelected() ? handles : ""} + {formatInstance._controlBtn && this.props.isSelected() ? handleLines : ""} </svg> ); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index a05a2b858..9ca8f348d 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -1,6 +1,7 @@ @import "globalCssVariables"; @import "nodeModuleOverrides"; + .dash-tooltip { font-size: 11px; padding: 2px; @@ -14,18 +15,23 @@ margin-top: 10px; } +.mainContent-div-flyout, .mainContent-div { position: relative; width: 100%; height: 100%; } +.mainContent-div-flyout { + left: calc(-1 * var(--flyoutHandleWidth)); +} // add nodes menu. Note that the + button is actually an input label, not an actual button. .mainView-docButtons { position: absolute; - bottom: 35px; + bottom: 10px; left: calc(100% + 5px); z-index: 1; + pointer-events: none; } .mainView-snapLines { @@ -110,13 +116,16 @@ height: 55px; width: 17px; position: absolute; - top: 55%; + top: 50%; border: 1px black solid; border-radius: 0; border-top-left-radius: 10px; border-bottom-left-radius: 10px; border-right: unset; - z-index: 2; + z-index: 1; + display: flex; + align-items: center; + padding: 4px; .mainView-propertiesDragger-icon { width: 10px; @@ -143,26 +152,38 @@ cursor: auto; } -.mainView-flyoutContainer { - display: flex; - flex-direction: column; +.mainView-innerContent { + display: contents; + flex-direction: row; position: relative; - height: 100%; - background: dimgray; + .mainView-flyoutContainer { + display: flex; + flex-direction: column; + position: relative; + height: 100%; + background: dimgray; - .documentView-node-topmost { - background: lightgrey; + .documentView-node-topmost { + background: lightgrey; + } + } + .propertiesView { + right: 0; + position: absolute; + z-index: 2; } } .mainView-menuPanel { - - width: 60px; + min-width: var(--menuPanelWidth); background-color: #121721; - height: calc(100% - $searchpanel-height); - //overflow-y: scroll; - //overflow-x: hidden; + .collectionStackingView { + scrollbar-width: none; + } + ::-webkit-scrollbar { + width:0; + } .mainView-menuPanel-button { padding: 7px; @@ -265,14 +286,22 @@ transform: none !important; } +.mainView-libraryFlyout-out, .mainView-libraryFlyout { height: 100%; width: 100%; - position: absolute; + position: relative; display: flex; flex-direction: column; z-index: 2; + .mainView-contentArea { + position: relative; + height: 100%; + width: 100%; + overflow: visible; + } + .mainView-libraryFlyout-close { right: 6; top: 5; @@ -282,26 +311,27 @@ margin-bottom: 10; } } - -.mainView-expandFlyoutButton { - position: absolute; - top: 120px; - right: 55px; - cursor: pointer; +.mainView-libraryFlyout-out { + transition: width .25s; + box-shadow: rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw; } + .mainView-libraryHandle { - width: 28px; - left: calc(100% - 10px); + width: var(--flyoutHandleWidth); height: 55px; top: 50%; + left: -10px; border: 1px solid black; border-radius: 8px; - position: absolute; - z-index: 2; + position: relative; + z-index: 1; touch-action: none; background-color: lightgrey; cursor: grab; + display: flex; + align-items: center; + padding: 12px; .mainView-libraryHandle-icon { width: 10px; @@ -313,7 +343,7 @@ } -.mainView-workspace { +.mainView-dashboard { height: 200px; position: relative; display: flex; @@ -325,8 +355,25 @@ display: flex; } -.mainView-recentlyClosed { +.mainView-inactiveDocs { height: 25%; position: relative; display: flex; +} + +.mainVew-webRef { + position: absolute; + left: -1000; + top: -1000; + display: block; + width: 200px; + height: 800px; +} +.mainVew-invisibleWebRef { + position: absolute; + left: 50; + top: 50; + display: block; + width: 500px; + height: 1000px; }
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 059e1f566..f9b3b1da8 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,48 +1,44 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faHireAHelper, faBuffer } from '@fortawesome/free-brands-svg-icons'; +import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons'; import * as fa from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; +import { action, computed, configure, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; -import Measure from 'react-measure'; -import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; -import { Id } from '../../fields/FieldSymbols'; +import * as ReactDOM from 'react-dom'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; -import { listSpec } from '../../fields/Schema'; -import { ScriptField } from '../../fields/ScriptField'; -import { BoolCast, Cast, FieldValue, StrCast } from '../../fields/Types'; -import { TraceMobx } from '../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils, simulateMouseClick } from '../../Utils'; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { PrefetchProxy } from '../../fields/Proxy'; +import { BoolCast, PromiseValue, StrCast } from '../../fields/Types'; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; -import { Docs, DocumentOptions } from '../documents/Documents'; +import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; import { DocumentManager } from '../util/DocumentManager'; -import GroupManager from '../util/GroupManager'; +import { GroupManager } from '../util/GroupManager'; import { HistoryUtil } from '../util/History'; +import { Hypothesis } from '../util/HypothesisUtils'; import { Scripting } from '../util/Scripting'; -import { SelectionManager } from '../util/SelectionManager'; -import SettingsManager from '../util/SettingsManager'; -import SharingManager from '../util/SharingManager'; +import { SettingsManager } from '../util/SettingsManager'; +import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { TimelineMenu } from './animationtimeline/TimelineMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; -import FormatShapePane from "./collections/collectionFreeForm/FormatShapePane"; -import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsMenu'; -import { PropertiesView } from './collections/collectionFreeForm/PropertiesView'; +import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; import { CollectionLinearView } from './collections/CollectionLinearView'; -import CollectionMenu from './collections/CollectionMenu'; -import { CollectionView, CollectionViewType } from './collections/CollectionView'; +import { CollectionMenu } from './collections/CollectionMenu'; +import { CollectionViewType } from './collections/CollectionView'; import { ContextMenu } from './ContextMenu'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; -import GestureOverlay from './GestureOverlay'; -import { ANTIMODEMENU_HEIGHT, SEARCH_PANEL_HEIGHT } from './globalCssVariables.scss'; -import KeyManager from './GlobalKeyHandler'; +import { InkStrokeProperties } from './InkStrokeProperties'; +import { GestureOverlay } from './GestureOverlay'; +import { MENU_PANEL_WIDTH, SEARCH_PANEL_HEIGHT } from './globalCssVariables.scss'; +import { KeyManager } from './GlobalKeyHandler'; import { LinkMenu } from './linking/LinkMenu'; import "./MainView.scss"; import { AudioBox } from './nodes/AudioBox'; @@ -53,70 +49,46 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; +import { WebBox } from './nodes/WebBox'; import { OverlayView } from './OverlayView'; -import PDFMenu from './pdf/PDFMenu'; +import { PDFMenu } from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; -import { Hypothesis } from '../util/HypothesisUtils'; -import { WebBox } from './nodes/WebBox'; -import * as ReactDOM from 'react-dom'; +import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; -import { SearchUtil } from '../util/SearchUtil'; -import { Networking } from '../Network'; -import * as rp from 'request-promise'; -import { LinkManager } from '../util/LinkManager'; -import RichTextMenu from './nodes/formattedText/RichTextMenu'; +import { TraceMobx } from '../../fields/util'; +import { SelectionManager } from '../util/SelectionManager'; +const _global = (window /* browser */ || global /* node */) as any; @observer export class MainView extends React.Component { public static Instance: MainView; - private _buttonBarHeight = 36; - private _flyoutSizeOnDown = 0; - private _urlState: HistoryUtil.DocUrl; private _docBtnRef = React.createRef<HTMLDivElement>(); private _mainViewRef = React.createRef<HTMLDivElement>(); + private _lastButton: Doc | undefined; @observable private _panelWidth: number = 0; @observable private _panelHeight: number = 0; - @observable private _flyoutTranslate: boolean = false; - @observable public flyoutWidth: number = 0; - private get darkScheme() { return BoolCast(Cast(this.userDoc?.activeWorkspace, Doc, null)?.darkScheme); } + @observable private _panelContent: string = "none"; + @observable private _sidebarContent: any = this.userDoc?.sidebar; + @observable private _flyoutWidth: number = 0; + @computed private get topOffset() { return (CollectionMenu.Instance?.Pinned ? 35 : 0) + Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } @computed private get userDoc() { return Doc.UserDoc(); } - @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; } + @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } + @computed private get mainContainer() { return this.userDoc ? CurrentUserUtils.ActiveDashboard : CurrentUserUtils.GuestDashboard; } @computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); } - @computed public get searchDoc() { return Cast(this.userDoc["search-panel"], Doc) as Doc; } - - @observable public sidebarContent: any = this.userDoc?.sidebar; - @observable public panelContent: string = "none"; - @observable public showProperties: boolean = false; - public isPointerDown = false; - @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } - } - - propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth)); - @computed get propertiesIcon() { - if (this.propertiesWidth() < 10) { - return "chevron-left"; - } else { - return "chevron-right"; - } - } - @observable propertiesDownX: number | undefined; + menuPanelWidth = () => Number(MENU_PANEL_WIDTH.replace("px", "")); + propertiesWidth = () => Math.max(0, Math.min(this._panelWidth - 50, CurrentUserUtils.propertiesWidth || 0)); componentDidMount() { + new InkStrokeProperties(); + this._sidebarContent.proto = undefined; DocServer.setPlaygroundFields(["dataTransition", "_viewTransition", "_panX", "_panY", "_viewScale", "_viewType", "_chromeStatus"]); // can play with these fields on someone else's - const tag = document.createElement('script'); - - const proto = DocServer.GetRefField("rtfProto").then(proto => { - (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), - msg => msg && alert(msg)); - }); + DocServer.GetRefField("rtfProto").then(proto => (proto instanceof Doc) && reaction(() => StrCast(proto.BROADCAST_MESSAGE), msg => msg && alert(msg))); + const tag = document.createElement('script'); tag.src = "https://www.youtube.com/iframe_api"; const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag); @@ -125,11 +97,7 @@ export class MainView extends React.Component { window.addEventListener("paste", KeyManager.Instance.paste as any); document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on const id = FormattedTextBox.GetDocFromUrl(e.detail); - DocServer.GetRefField(id).then(doc => { - if (doc instanceof Doc) { - DocumentManager.Instance.jumpToDocument(doc, false, undefined); - } - }); + DocServer.GetRefField(id).then(doc => (doc instanceof Doc) ? DocumentManager.Instance.jumpToDocument(doc, false, undefined) : (null)); }); document.addEventListener("linkAnnotationToDash", Hypothesis.linkListener); } @@ -137,7 +105,6 @@ export class MainView extends React.Component { componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); window.removeEventListener("pointerdown", this.globalPointerDown); - window.removeEventListener("pointerup", this.globalPointerUp); window.removeEventListener("paste", KeyManager.Instance.paste as any); document.removeEventListener("linkAnnotationToDash", Hypothesis.linkListener); } @@ -145,212 +112,125 @@ export class MainView extends React.Component { constructor(props: Readonly<{}>) { super(props); MainView.Instance = this; - this._urlState = HistoryUtil.parseUrl(window.location) || {} as any; - // causes errors to be generated when modifying an observable outside of an action - - CurrentUserUtils.propertiesWidth = 0; + CurrentUserUtils._urlState = HistoryUtil.parseUrl(window.location) || {} as any; + // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); + if (window.location.pathname !== "/home") { const pathname = window.location.pathname.substr(1).split("/"); - if (pathname.length > 1) { - const type = pathname[0]; - if (type === "doc") { - CurrentUserUtils.MainDocId = pathname[1]; - if (!this.userDoc) { - runInAction(() => this.closeFlyout()); - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) => - field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); - } - } + if (pathname.length > 1 && pathname[0] === "doc") { + CurrentUserUtils.MainDocId = pathname[1]; + !this.userDoc && DocServer.GetRefField(pathname[1]).then(action(field => field instanceof Doc && (CurrentUserUtils.GuestTarget = field))); } } library.add(fa.faEdit, fa.faTrash, fa.faTrashAlt, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faCalendar, - fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, - fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, - fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight, + fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faMapMarker, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, + fa.faLock, fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointLeft, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone, fa.faKeyboard, + fa.faQuestion, fa.faTasks, fa.faPalette, fa.faAngleLeft, fa.faAngleRight, fa.faBell, fa.faCamera, fa.faExpand, fa.faCaretDown, fa.faCaretLeft, fa.faCaretRight, fa.faCaretSquareDown, fa.faCaretSquareRight, fa.faArrowsAltH, fa.faPlus, fa.faMinus, fa.faTerminal, fa.faToggleOn, fa.faFile, fa.faLocationArrow, - fa.faSearch, fa.faFileDownload, fa.faStop, fa.faCalculator, fa.faWindowMaximize, fa.faAddressCard, fa.faQuestionCircle, fa.faArrowLeft, - fa.faArrowRight, fa.faArrowDown, fa.faArrowUp, fa.faBolt, fa.faBullseye, fa.faCaretUp, fa.faCat, fa.faCheck, fa.faChevronRight, fa.faClipboard, + fa.faSearch, fa.faFileDownload, fa.faFileUpload, fa.faStop, fa.faCalculator, fa.faWindowMaximize, fa.faAddressCard, fa.faQuestionCircle, fa.faArrowLeft, + fa.faArrowRight, fa.faArrowDown, fa.faArrowUp, fa.faBolt, fa.faBullseye, fa.faCaretUp, fa.faCat, fa.faCheck, fa.faChevronRight, fa.faChevronLeft, fa.faChevronDown, fa.faChevronUp, fa.faClone, fa.faCloudUploadAlt, fa.faCommentAlt, fa.faCompressArrowsAlt, fa.faCut, fa.faEllipsisV, fa.faEraser, fa.faExclamation, fa.faFileAlt, - fa.faFileAudio, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer, + fa.faFileAudio, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAmericas, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer, fa.faMusic, fa.faObjectGroup, fa.faPause, fa.faPen, fa.faPenNib, fa.faPhone, fa.faPlay, fa.faPortrait, fa.faRedoAlt, fa.faStamp, fa.faStickyNote, fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined, - fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faChevronLeft, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, + fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faClipboard, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript, fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper, fa.faDesktop, fa.faTrashRestore, fa.faUsers, fa.faWrench, fa.faCog, fa.faMap, fa.faBellSlash, fa.faExpandAlt, fa.faArchive, fa.faBezierCurve, fa.faCircle, fa.faLongArrowAltRight, fa.faPenFancy, fa.faAngleDoubleRight, faBuffer, fa.faExpand, fa.faUndo, fa.faSlidersH, fa.faAngleDoubleLeft, fa.faAngleUp, fa.faAngleDown, fa.faPlayCircle, fa.faClock, fa.faRocket, fa.faExchangeAlt, faBuffer, fa.faHashtag, fa.faAlignJustify, fa.faCheckSquare, fa.faListUl, - fa.faWindowMinimize, fa.faWindowRestore); + fa.faWindowMinimize, fa.faWindowRestore, fa.faTextWidth, fa.faTextHeight, fa.faClosedCaptioning, fa.faInfoCircle, fa.faTag, fa.faSyncAlt, fa.faPhotoVideo, + fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical, + fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, + fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines); this.initEventListeners(); this.initAuthenticationRouters(); } globalPointerDown = action((e: PointerEvent) => { - this.isPointerDown = true; AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); - if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) { - ContextMenu.Instance.closeMenu(); - } - if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) { - TimelineMenu.Instance.closeMenu(); - } - if (targets && targets.length && SearchBox.Instance._searchbarOpen) { - let check = false; - const icon = "icon"; - targets.forEach((thing) => { - if (thing.className.toString() === "collectionSchemaView-searchContainer" || (thing as any)?.dataset[icon] === "filter" || thing.className.toString() === "collectionSchema-header-menuOptions" || thing.className.toString() === "altcollectionTimeView-treeView") { - check = true; - } - }); - if (check === false) { - SearchBox.Instance.closeSearch(); + if (targets.length) { + const targClass = targets[0].className.toString(); + if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) { + const check = targets.some((thing) => + (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || + thing.className === "collectionSchema-header-menuOptions")); + !check && SearchBox.Instance.resetSearch(true); } + !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu(); + !["timeline-menu-desc", "timeline-menu-item", "timeline-menu-input"].includes(targClass) && TimelineMenu.Instance.closeMenu(); } - - }); - globalPointerUp = () => this.isPointerDown = false; - initEventListeners = () => { - window.addEventListener("drop", (e) => { e.preventDefault(); }, false); // drop event handler - window.addEventListener("dragover", (e) => { e.preventDefault(); }, false); // drag event handler - // click interactions for the context menu + window.addEventListener("drop", e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page + window.addEventListener("dragover", e => e.preventDefault(), false); document.addEventListener("pointerdown", this.globalPointerDown); - document.addEventListener("pointerup", this.globalPointerUp); + document.addEventListener("click", (e: MouseEvent) => { + if (!e.cancelBubble) { + const pathstr = (e as any)?.path.map((p: any) => p.classList?.toString()).join(); + if (pathstr.includes("libraryFlyout")) { + SelectionManager.DeselectAll(); + } + } + }); } initAuthenticationRouters = async () => { - // Load the user's active workspace, or create a new one if initial session after signup + // Load the user's active dashboard, or create a new one if initial session after signup const received = CurrentUserUtils.MainDocId; if (received && !this.userDoc) { - reaction( - () => CurrentUserUtils.GuestTarget, - target => target && this.createNewWorkspace(), - { fireImmediately: true } - ); + reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(Doc.UserDoc()), { fireImmediately: true }); } else { - if (received && this._urlState.sharing) { + if (received && CurrentUserUtils._urlState.sharing) { reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized, initialized => initialized && received && DocServer.GetRefField(received).then(docField => { if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) { - CollectionDockingView.AddRightSplit(docField); + CollectionDockingView.AddSplit(docField, "right"); } }), ); } - const doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc); - if (doc) { - this.openWorkspace(doc); - } else { - this.createNewWorkspace(); - } - } - } - - @action - createNewWorkspace = async (id?: string) => { - const workspaces = Cast(this.userDoc.myWorkspaces, Doc) as Doc; - const workspaceCount = DocListCast(workspaces.data).length + 1; - const freeformOptions: DocumentOptions = { - x: 0, - y: 400, - _width: this._panelWidth * .7 - this.propertiesWidth() * 0.7, - _height: this._panelHeight, - title: "Untitled Collection", - }; - const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); - const workspaceDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myCatalog as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); - - const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); - const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); - const copyWorkspace = ScriptField.MakeScript(`copyWorkspace()`); - workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, copyWorkspace!]); - workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "Snapshot Workspace"]); - - Doc.AddDocToList(workspaces, "data", workspaceDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => this.openWorkspace(workspaceDoc), 0); - } - - @action - openWorkspace = (doc: Doc, fromHistory = false) => { - CurrentUserUtils.MainDocId = doc[Id]; - - if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest workspace - !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })])); - this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc); - } - const state = this._urlState; - if (state.sharing === true && !this.userDoc) { - DocServer.Control.makeReadOnly(); - } else { - fromHistory || HistoryUtil.pushState({ - type: "doc", - docId: doc[Id], - readonly: state.readonly, - nro: state.nro, - sharing: false, + const activeDash = PromiseValue(this.userDoc.activeDashboard); + activeDash.then(dash => { + if (dash instanceof Doc) CurrentUserUtils.openDashboard(this.userDoc, dash); + else CurrentUserUtils.createNewDashboard(this.userDoc); }); - if (state.readonly === true || state.readonly === null) { - DocServer.Control.makeReadOnly(); - } else if (state.safe) { - if (!state.nro) { - DocServer.Control.makeReadOnly(); - } - CollectionView.SetSafeMode(true); - } else if (state.nro || state.nro === null || state.readonly === false) { - } else if (doc.readOnly) { - DocServer.Control.makeReadOnly(); - } else { - DocServer.Control.makeEditable(); - } } - - return true; - } - - onDrop = (e: React.DragEvent<HTMLDivElement>) => { - e.preventDefault(); - e.stopPropagation(); } @action - onResize = (r: any) => { - this._panelWidth = r.offset.width;// - this.propertiesWidth(); - this._panelHeight = r.offset.height; + createNewPresentation = async () => { + if (!await this.userDoc.myPresentations) { + this.userDoc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "PRESENTATION TRAILS", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, treeViewOpen: true, system: true + })); + } + const pres = Docs.Create.PresDocument(new List<Doc>(), + { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias", _chromeStatus: "replaced", boxShadow: "0 0", system: true }); + CollectionDockingView.AddSplit(pres, "right"); + this.userDoc.activePresentation = pres; + Doc.AddDocToList(this.userDoc.myPresentations as Doc, "data", pres); } - @action - getPWidth = () => this._panelWidth - this.propertiesWidth() - + getPWidth = () => this._panelWidth - this.propertiesWidth(); getPHeight = () => this._panelHeight; - getContentsHeight = () => this._panelHeight - this._buttonBarHeight; + getContentsHeight = () => this._panelHeight - Number(SEARCH_PANEL_HEIGHT.replace("px", "")); defaultBackgroundColors = (doc: Opt<Doc>, renderDepth: number) => { - if (this.panelContent === doc?.title) return "lightgrey"; - if (doc?.type === DocumentType.COL) { - if (doc.title === "Basic Item Creators" || doc.title === "sidebar-tools" - || doc.title === "sidebar-recentlyClosed" || doc.title === "sidebar-catalog" - || doc.title === "Mobile Uploads" || doc.title === "COLLECTION_PROTO" - || doc.title === "Advanced Item Prototypes" || doc.title === "all Creators") { - return "lightgrey"; - } - return StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); + return Doc.IsSystem(doc) ? "lightgrey" : StrCast(renderDepth > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground); } if (this.darkScheme) { switch (doc?.type) { case DocumentType.FONTICON: return "white"; case DocumentType.RTF || DocumentType.LABEL || DocumentType.BUTTON: return "#2d2d2d"; case DocumentType.LINK: - case DocumentType.COL: { - if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - } + case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; default: return "black"; } } else { @@ -360,9 +240,7 @@ export class MainView extends React.Component { case DocumentType.BUTTON: case DocumentType.LABEL: return "lightgray"; case DocumentType.LINK: - case DocumentType.COL: { - if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; - } + case DocumentType.COL: if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray"; default: return "white"; } } @@ -391,6 +269,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} renderDepth={-1} @@ -398,60 +277,41 @@ export class MainView extends React.Component { } @computed get dockingContent() { - TraceMobx(); - const mainContainer = this.mainContainer; - const width = this.flyoutWidth + this.propertiesWidth(); - return <div className="mainContent-div" onDrop={this.onDrop} style={{ width: `calc(100% - ${width}px)`, height: `calc(100% - ${SEARCH_PANEL_HEIGHT})` }}> - {!mainContainer ? (null) : this.mainDocView} + return <div className={`mainContent-div${this._flyoutWidth ? "-flyout" : ""}`} onDrop={e => { e.stopPropagation(); e.preventDefault(); }} + style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` }}> + {!this.mainContainer ? (null) : this.mainDocView} </div>; } @action onPropertiesPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - CurrentUserUtils.propertiesWidth = this._panelWidth - e.clientX; - return false; - }), returnFalse, action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); + setupMoveUpEvents(this, e, + action(e => (CurrentUserUtils.propertiesWidth = Math.max(0, this._panelWidth - e.clientX)) ? false : false), + action(() => CurrentUserUtils.propertiesWidth < 5 && (CurrentUserUtils.propertiesWidth = 0)), + action(() => CurrentUserUtils.propertiesWidth = this.propertiesWidth() < 15 ? Math.min(this._panelWidth - 50, 250) : 0), false); } @action onFlyoutPointerDown = (e: React.PointerEvent) => { - if (this._flyoutTranslate) { - setupMoveUpEvents(this, e, action((e: PointerEvent) => { - this.flyoutWidth = Math.max(e.clientX, 0); - if (this.flyoutWidth < 5) { - this.panelContent = "none"; - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - } - return false; - }), emptyFunction, action(() => { - if (this.flyoutWidth < 15) MainView.expandFlyout(); - else this.closeFlyout(); - })); - } + setupMoveUpEvents(this, e, + action(e => (this._flyoutWidth = Math.max(e.clientX - 58, 0)) ? false : false), + () => this._flyoutWidth < 5 && this.closeFlyout(), + this.closeFlyout); } - flyoutWidthFunc = () => this.flyoutWidth; - addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { - return where === "close" ? CollectionDockingView.CloseRightSplit(doc) : - doc.dockingConfig ? this.openWorkspace(doc) : - CollectionDockingView.AddRightSplit(doc, libraryPath); - } + flyoutWidthFunc = () => this._flyoutWidth; sidebarScreenToLocal = () => new Transform(0, (CollectionMenu.Instance.Pinned ? -35 : 0) - Number(SEARCH_PANEL_HEIGHT.replace("px", "")), 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-58, 0); + addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => { + return where === "close" ? CollectionDockingView.CloseSplit(doc) : + doc.dockingConfig ? CurrentUserUtils.openDashboard(Doc.UserDoc(), doc) : CollectionDockingView.AddSplit(doc, "right"); + } @computed get flyout() { - if (!this.sidebarContent) return null; - return <div className="mainView-libraryFlyout"> - <div className="mainView-contentArea" style={{ position: "relative", height: `calc(100% - ${SEARCH_PANEL_HEIGHT})`, width: "100%", overflow: "visible" }}> - {/* {this.flyoutWidth > 0 ? <div className="mainView-libraryFlyout-close" - onPointerDown={this.closeFlyout}> - <FontAwesomeIcon icon="times" color="black" size="lg" /> - </div> : null} */} - + return <div className={`mainView-libraryFlyout${this._flyoutWidth ? "" : "-out"}`} style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} > + <div className="mainView-contentArea" > <DocumentView - Document={this.sidebarContent} + Document={this._sidebarContent} DataDoc={undefined} LibraryPath={emptyPath} addDocument={undefined} @@ -473,13 +333,15 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} forcedBackgroundColor={() => "lightgrey"} /> </div> - {this.docButtons}</div>; + {this.docButtons} + </div>; } @computed get menuPanel() { @@ -498,7 +360,7 @@ export class MainView extends React.Component { onClick={undefined} ScreenToLocalTransform={this.sidebarScreenToLocal} ContentScaling={returnOne} - PanelWidth={() => 60} + PanelWidth={this.menuPanelWidth} PanelHeight={this.getContentsHeight} renderDepth={0} focus={emptyFunction} @@ -507,6 +369,7 @@ export class MainView extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} relative={true} @@ -515,159 +378,94 @@ export class MainView extends React.Component { </div>; } - - @action - closeFlyout = () => { - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - this.panelContent = "none"; - this.flyoutWidth = 0; - } - - get groupManager() { return GroupManager.Instance; } - - _lastButton: Doc | undefined; @action selectMenu = (button: Doc) => { const title = StrCast(Doc.GetProto(button).title); - this._lastButton && (this._lastButton.color = "white"); - this._lastButton && (this._lastButton._backgroundColor = ""); - if (this.panelContent === title && this.flyoutWidth !== 0) { - this.panelContent = "none"; - this.flyoutWidth = 0; - } else { - let panelDoc: Doc | undefined; - switch (this.panelContent = title) { - case "Tools": panelDoc = Doc.UserDoc()["sidebar-tools"] as Doc ?? undefined; break; - case "Workspace": panelDoc = Doc.UserDoc()["sidebar-workspaces"] as Doc ?? undefined; break; - case "Catalog": panelDoc = Doc.UserDoc()["sidebar-catalog"] as Doc ?? undefined; break; - case "Archive": panelDoc = Doc.UserDoc()["sidebar-recentlyClosed"] as Doc ?? undefined; break; - case "Settings": SettingsManager.Instance.open(); break; - case "Import": panelDoc = Doc.UserDoc()["sidebar-import"] as Doc ?? undefined; break; - case "Sharing": panelDoc = Doc.UserDoc()["sidebar-sharing"] as Doc ?? undefined; break; - case "User Doc": panelDoc = Doc.UserDoc()["sidebar-userDoc"] as Doc ?? undefined; break; + const willOpen = !this._flyoutWidth || this._panelContent !== title; + this.closeFlyout(); + if (willOpen) { + switch (this._panelContent = title) { + case "Settings": + SettingsManager.Instance.open(); + break; + case "Catalog": + SearchBox.Instance._searchFullDB = "My Stuff"; + SearchBox.Instance.enter(undefined); + break; + default: + this.expandFlyout(button); } - this.sidebarContent.proto = panelDoc; - if (panelDoc) { - MainView.expandFlyout(); - button._backgroundColor = "lightgrey"; - button.color = "black"; - this._lastButton = button; - } else this.flyoutWidth = 0; } return true; } - @action - closeProperties = () => { - CurrentUserUtils.propertiesWidth = 0; - } - - @computed get propertiesView() { - TraceMobx(); - return <div className="mainView-propertiesView" style={{ - overflow: this.propertiesWidth() < 15 ? "hidden" : undefined - }}> - <PropertiesView - width={this.propertiesWidth()} - height={this._panelHeight} - renderDepth={1} - ScreenToLocalTransform={Transform.Identity} - onDown={this.closeProperties} - /> - </div>; - } - @computed get mainInnerContent() { - const rightFlyout = this.propertiesWidth() - 1; return <> {this.menuPanel} - <div style={{ display: "contents", flexDirection: "row", position: "relative" }}> - <div className="mainView-flyoutContainer" style={{ width: this.flyoutWidth }}> - {this.flyoutWidth !== 0 ? <div className="mainView-libraryHandle" - onPointerDown={this.onFlyoutPointerDown} - //style={{ backgroundColor: '#8c8b8b' }} - > - <span title="library View Dragger" style={{ - width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw", - //height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh", - position: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "absolute" : "fixed", - top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "0" - }} /> - <div className="mainview-libraryHandle-icon"> - <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> - </div> - </div> : null} - <div className="mainView-libraryFlyout" style={{ - //transformOrigin: this._flyoutTranslate ? "" : "left center", - transition: this._flyoutTranslate ? "" : "width .5s", - //transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`, - boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.2vw" - }}> - {this.flyout} - {this.expandButton} - </div> + <div className="mainView-innerContent" > + {this.flyout} + <div className="mainView-libraryHandle" style={{ display: !this._flyoutWidth ? "none" : undefined }} onPointerDown={this.onFlyoutPointerDown} > + <FontAwesomeIcon icon="chevron-left" color="black" size="sm" /> </div> + {this.dockingContent} - {this.showProperties ? (null) : - <div className="mainView-propertiesDragger" title="Properties View Dragger" onPointerDown={this.onPropertiesPointerDown} - style={{ right: rightFlyout, top: "50%" }}> - <div className="mainView-propertiesDragger-icon"> - <FontAwesomeIcon icon={this.propertiesIcon} color="black" size="sm" /> </div> - </div> - } - {this.propertiesWidth() < 10 ? (null) : - <div style={{ width: this.propertiesWidth(), height: "calc(100% - 35px)" }}> {this.propertiesView} </div>} + + <div className="mainView-propertiesDragger" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? "chevron-left" : "chevron-right"} color="black" size="sm" /> + </div> + {this.propertiesWidth() < 10 ? (null) : <PropertiesView width={this.propertiesWidth()} height={this.getContentsHeight()} />} </div> </>; } @computed get mainContent() { - const n = (CollectionMenu.Instance?.Pinned ? 1 : 0); - const height = `calc(100% - ${n * Number(ANTIMODEMENU_HEIGHT.replace("px", ""))}px)`; - const pinned = FormatShapePane.Instance?.Pinned; - const innerContent = this.mainInnerContent; - return !this.userDoc ? (null) : ( - <Measure offset onResize={this.onResize}> - {({ measureRef }) => - <div className="mainView-mainContent" ref={measureRef} style={{ - color: this.darkScheme ? "rgb(205,205,205)" : "black", - //change to times 2 for both pinned - height, - width: pinned ? `calc(100% - 200px)` : "100%" - }} > - {innerContent} - </div> - } - </Measure>); + return !this.userDoc ? (null) : + <div className="mainView-mainContent" ref={r => { + r && new _global.ResizeObserver(action(() => { this._panelWidth = r.getBoundingClientRect().width; this._panelHeight = r.getBoundingClientRect().height; })).observe(r); + }} style={{ + color: this.darkScheme ? "rgb(205,205,205)" : "black", + height: `calc(100% - ${this.topOffset}px)`, + width: "100%", + }} > + {this.mainInnerContent} + </div>; } - public static expandFlyout = action(() => { - MainView.Instance._flyoutTranslate = true; - MainView.Instance.flyoutWidth = (MainView.Instance.flyoutWidth || 250); - + expandFlyout = action((button: Doc) => { + this._flyoutWidth = (this._flyoutWidth || 250); + this._sidebarContent.proto = button.target as any; + button._backgroundColor = "lightgrey"; + button.color = "black"; + this._lastButton = button; }); - @computed get expandButton() { - return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}></div>) : (null); - } + closeFlyout = action(() => { + this._lastButton && (this._lastButton.color = "white"); + this._lastButton && (this._lastButton._backgroundColor = ""); + this._panelContent = "none"; + this._sidebarContent.proto = undefined; + this._flyoutWidth = 0; + }); - addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.UserDoc().dockedBtns as Doc, "data", doc), true); moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); + addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => { + const ret = flg && Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data", doc); + ret && (doc._stayInCollection = undefined); + return ret; + }, true) buttonBarXf = () => { if (!this._docBtnRef.current) return Transform.Identity(); const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); return new Transform(-translateX, -translateY, 1 / scale); } + @computed get docButtons() { - const dockedBtns = Doc.UserDoc()?.dockedBtns; - if (dockedBtns instanceof Doc) { - return <div className="mainView-docButtons" ref={this._docBtnRef} - style={{ height: !dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > + return !(this.userDoc.dockedBtns instanceof Doc) ? (null) : + <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !this.userDoc.dockedBtns.linearViewIsExpanded ? "42px" : undefined }} > <CollectionLinearView - Document={dockedBtns} + Document={this.userDoc.dockedBtns} DataDoc={undefined} LibraryPath={emptyPath} fieldKey={"data"} @@ -696,23 +494,13 @@ export class MainView extends React.Component { focus={emptyFunction} whenActiveChanged={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; - } - return (null); - } - - get mainViewElement() { - return document.getElementById("mainView-container"); - } - - get mainViewRef() { - return this._mainViewRef; } - @computed get snapLines() { - return !Doc.UserDoc().showSnapLines ? (null) : <div className="mainView-snapLines"> + return !this.userDoc.showSnapLines ? (null) : <div className="mainView-snapLines"> <svg style={{ width: "100%", height: "100%" }}> {SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} {SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)} @@ -742,12 +530,18 @@ export class MainView extends React.Component { </defs> </svg>; } + select = (ctrlPressed: boolean) => { }; @computed get search() { + TraceMobx(); return <div className="mainView-searchPanel"> - {/* <div style={{ float: "left", marginLeft: "10px" }}>{Doc.CurrentUserEmail}</div> */} - <div><DocumentView Document={this.searchDoc} - DataDoc={undefined} + <SearchBox Document={CurrentUserUtils.MySearchPanelDoc} + DataDoc={CurrentUserUtils.MySearchPanelDoc} + fieldKey="data" + dropAction="move" + isSelected={returnTrue} + active={returnTrue} + select={this.select} LibraryPath={emptyPath} addDocument={undefined} addDocTab={this.addDocTabFunc} @@ -764,19 +558,18 @@ export class MainView extends React.Component { PanelHeight={this.getPHeight} renderDepth={0} focus={emptyFunction} - parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - /></div> + ContainingCollectionDoc={undefined} /> </div>; } @computed get invisibleWebBox() { // see note under the makeLink method in HypothesisUtils.ts return !DocumentLinksButton.invisibleWebDoc ? null : - <div style={{ position: 'absolute', left: 50, top: 50, display: 'block', width: '500px', height: '1000px' }} ref={DocumentLinksButton.invisibleWebRef}> + <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}> <WebBox fieldKey={"data"} ContainingCollectionView={undefined} @@ -801,13 +594,13 @@ export class MainView extends React.Component { NativeWidth={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} /> </div>; } render() { - return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}> - + return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} onScroll={() => document.getElementById("root")!.scrollTop = 0} ref={this._mainViewRef}> {this.inkResources} <DictationOverlay /> <SharingManager /> @@ -817,8 +610,6 @@ export class MainView extends React.Component { <DocumentDecorations /> {this.search} <CollectionMenu /> - <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> - <FormatShapePane /> {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.EditLink ? <LinkMenu docView={DocumentLinksButton.EditLink} addDocTab={DocumentLinksButton.EditLink.props.addDocTab} changeFlyout={emptyFunction} /> : (null)} {LinkDocPreview.LinkInfo ? <LinkDocPreview location={LinkDocPreview.LinkInfo.Location} backgroundColor={this.defaultBackgroundColors} @@ -830,15 +621,13 @@ export class MainView extends React.Component { <PreviewCursor /> <TaskCompletionBox /> <ContextMenu /> - <FormatShapePane /> <RadialMenu /> <PDFMenu /> <MarqueeOptionsMenu /> - <OverlayView /> <TimelineMenu /> {this.snapLines} - <div ref={this.makeWebRef} style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} /> + <div className="mainView-webRef" ref={this.makeWebRef} /> </div >); } @@ -847,7 +636,7 @@ export class MainView extends React.Component { invisibleDoc => { ReactDOM.unmountComponentAtNode(ele); invisibleDoc && ReactDOM.render(<span title="Drag as document" className="invisible-webbox" > - <div style={{ position: 'absolute', left: -1000, top: -1000, display: 'block', width: '200px', height: '800px' }} ref={DocumentLinksButton.invisibleWebRef}> + <div className="mainView-webRef" ref={DocumentLinksButton.invisibleWebRef}> <WebBox fieldKey={"data"} ContainingCollectionView={undefined} @@ -872,112 +661,26 @@ export class MainView extends React.Component { NativeWidth={() => 800} ContentScaling={returnOne} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} /> </div>; </span>, ele); - var success = false; + let success = false; const onSuccess = () => { success = true; clearTimeout(interval); document.removeEventListener("editSuccess", onSuccess); }; - // For some reason, Hypothes.is annotations don't load until a click is registered on the page, + // For some reason, Hypothes.is annotations don't load until a click is registered on the page, // so we keep simulating clicks until annotations have loaded and editing is successful - const interval = setInterval(() => { - !success && simulateMouseClick(ele, 50, 50, 50, 50); - }, 500); - + const interval = setInterval(() => !success && simulateMouseClick(ele, 50, 50, 50, 50), 500); setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s document.addEventListener("editSuccess", onSuccess); }); } - - importDocument = () => { - const sidebar = Cast(Doc.UserDoc()["sidebar-import-documents"], Doc, null); - const sidebarDocView = DocumentManager.Instance.getDocumentView(sidebar); - const input = document.createElement("input"); - input.type = "file"; - input.multiple = true; - input.accept = ".zip, application/pdf, video/*, image/*, audio/*"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file && file.type === 'application/zip') { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc && sidebarDocView) { - sidebarDocView.props.addDocument?.(doc); - setTimeout(() => { - SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { - docs.docs.forEach(d => LinkManager.Instance.addLink(d)); - }); - }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. - - } - } - } else if (input.files && input.files.length !== 0) { - const files = input.files || []; - Array.from(files).forEach(async file => { - const res = await Networking.UploadFilesToServer(file); - res.map(async ({ result }) => { - const name = file.name; - if (result instanceof Error) { - return; - } - const path = Utils.prepend(result.accessPaths.agnostic.client); - let doc: Doc; - // Case 1: File is a video - if (file.type.includes("video")) { - doc = Docs.Create.VideoDocument(path, { _height: 100, title: name }); - // Case 2: File is a PDF document - } else if (file.type === "application/pdf") { - doc = Docs.Create.PdfDocument(path, { _height: 100, _fitWidth: true, title: name }); - // Case 3: File is an image - } else if (file.type.includes("image")) { - doc = Docs.Create.ImageDocument(path, { _height: 100, title: name }); - // Case 4: File is an audio document - } else { - doc = Docs.Create.AudioDocument(path, { title: name }); - } - const res = await rp.get(Utils.prepend("/getUserDocumentId")); - if (!res) { - throw new Error("No user id returned"); - } - const field = await DocServer.GetRefField(res); - let pending: Opt<Doc>; - if (field instanceof Doc) { - pending = sidebar; - } - if (pending) { - const data = await Cast(pending.data, listSpec(Doc)); - if (data) data.push(doc); - else pending.data = new List([doc]); - } - }); - }); - } else { - console.log("No file selected"); - } - }; - input.click(); - } } + Scripting.addGlobal(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); -Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); -Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }); -Scripting.addGlobal(function copyWorkspace() { - const copiedWorkspace = Doc.MakeCopy(Cast(Doc.UserDoc().activeWorkspace, Doc, null), true); - const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc, null); - Doc.AddDocToList(workspaces, "data", copiedWorkspace); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => MainView.Instance.openWorkspace(copiedWorkspace), 0); -}); -Scripting.addGlobal(function importDocument() { return MainView.Instance.importDocument(); }, - "imports files from device directly into the import sidebar"); +Scripting.addGlobal(function toggleComicMode() { Doc.UserDoc().fontFamily = "Comic Sans MS"; Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; });
\ No newline at end of file diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 19387f619..34e0ff126 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -14,7 +14,7 @@ export interface MainViewOverlayProps { } @observer -export default class MainViewModal extends React.Component<MainViewOverlayProps> { +export class MainViewModal extends React.Component<MainViewOverlayProps> { render() { const p = this.props; diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 49580cde4..cc0dd0443 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,10 +1,11 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import * as React from "react"; +import ReactLoading from 'react-loading'; import { Doc, DocListCast, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; import { NumCast, Cast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, Utils, setupMoveUpEvents, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView"; import { DocumentView } from "./nodes/DocumentView"; @@ -145,7 +146,7 @@ export class OverlayView extends React.Component { @computed get overlayDocs() { - const userDocOverlays = Doc.UserDoc().myOverlayDocuments; + const userDocOverlays = Doc.UserDoc().myOverlayDocs; if (!userDocOverlays) { return null; } @@ -165,7 +166,7 @@ export class OverlayView extends React.Component { dragData.dropAction = "move"; dragData.removeDocument = (doc: Doc | Doc[]) => { const docs = (doc instanceof Doc) ? [doc] : doc; - docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null), "data", d)); + docs.forEach(d => Doc.RemoveDocFromList(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", d)); return true; }; dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { @@ -205,12 +206,18 @@ export class OverlayView extends React.Component { addDocTab={returnFalse} pinToPres={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; }); } + public static ShowSpinner() { + return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 }); + } + + render() { return ( <div className="overlayView" id="overlayView"> diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 0a4334302..92c3f09b4 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Doc } from "../../fields/Doc"; import { NumCast } from "../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter } from "../../Utils"; +import { emptyFunction, emptyPath, returnEmptyString, returnZero, returnFalse, returnOne, returnTrue, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { DocumentView } from "./nodes/DocumentView"; import "./Palette.scss"; @@ -60,6 +60,7 @@ export default class Palette extends React.Component<PaletteProps> { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index d7034fcfb..1cadba18a 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -8,7 +8,7 @@ import { Doc } from '../../fields/Doc'; import { Transform } from "../util/Transform"; import { DocServer } from '../DocServer'; import { undoBatch, UndoManager } from '../util/UndoManager'; -import { NumCast } from '../../fields/Types'; +import { NumCast, Cast } from '../../fields/Types'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import * as rp from 'request-promise'; import { Utils } from '../../Utils'; @@ -44,8 +44,7 @@ export class PreviewCursor extends React.Component<{}> { if (plain.indexOf("www.youtube.com/watch") !== -1) { const url = plain.replace("youtube.com/watch?v=", "youtube.com/embed/"); undoBatch(() => PreviewCursor._addDocument(Docs.Create.VideoDocument(url, { - title: url, _width: 400, _height: 315, - _nativeWidth: 600, _nativeHeight: 472.5, + title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5, x: newPoint[0], y: newPoint[1] })))(); } @@ -53,45 +52,41 @@ export class PreviewCursor extends React.Component<{}> { else if (re.test(plain)) { const url = plain; undoBatch(() => PreviewCursor._addDocument(Docs.Create.WebDocument(url, { - title: url, _width: 500, _height: 300, UseCors: true, - // nativeWidth: 300, nativeHeight: 472.5, - x: newPoint[0], y: newPoint[1] + title: url, _width: 500, _height: 300, useCors: true, x: newPoint[0], y: newPoint[1] })))(); } - else if (plain.startsWith("__DashDocId(") || plain.startsWith("__DashCloneId(")) { const clone = plain.startsWith("__DashCloneId("); const docids = plain.split(":"); const strs = docids[0].split(","); const ptx = Number(strs[0].substring((clone ? "__DashCloneId(" : "__DashDocId(").length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); - let count = 1; - const list: Doc[] = []; - let first: Doc | undefined; - docids.map((did, i) => i && DocServer.GetRefField(did).then(async doc => { - count++; - if (doc instanceof Doc) { - i === 1 && (first = doc); - const alias = clone ? (await Doc.MakeClone(doc)).clone : doc; - const deltaX = NumCast(doc.x) - NumCast(first!.x) - ptx; - const deltaY = NumCast(doc.y) - NumCast(first!.y) - pty; - alias.x = newPoint[0] + deltaX; - alias.y = newPoint[1] + deltaY; - list.push(alias); - } - if (count === docids.length) { - undoBatch(() => PreviewCursor._addDocument(list))(); - } - })); + const batch = UndoManager.StartBatch("cloning"); + { + const docs = await Promise.all(docids.filter((did, i) => i).map(async (did) => { + const doc = Cast(await DocServer.GetRefField(did), Doc, null); + return clone ? (await Doc.MakeClone(doc)).clone : doc; + })); + const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0; + const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0; + docs.map(doc => { + doc.x = NumCast(doc.x) - firstx; + doc.y = NumCast(doc.y) - firsty; + }); + PreviewCursor._addDocument(docs); + } + batch.end(); e.stopPropagation(); - } else { + } + else { // creates text document FormattedTextBox.PasteOnLoad = e; undoBatch(() => PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", { _width: 500, limitHeight: 400, _autoHeight: true, + _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, x: newPoint[0], y: newPoint[1], title: "-pasted text-" @@ -132,6 +127,7 @@ export class PreviewCursor extends React.Component<{}> { e.key !== "Insert" && e.key !== "Home" && e.key !== "End" && e.key !== "PageUp" && e.key !== "PageDown" && e.key !== "NumLock" && e.key !== " " && (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys + (e.keyCode < 173 || e.keyCode > 183 || e.key === "-") && // mute, volume up/down etc, - is there specifically because its keycode is 173 in Firefox so shouldn't be avoided !e.key.startsWith("Arrow") && !e.defaultPrevented) { if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {// /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) { diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index 8d9d56c9e..d63eb874c 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -43,6 +43,13 @@ $linkGap : 3px; cursor: pointer; } } +.propertiesButtons-linkButton-empty.toggle-on { + background-color: white; + color: black; +} +.propertiesButtons-linkButton-empty.toggle-off { + color: white; +} .propertiesButtons { margin-top: 3px; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 35d4f7f6e..d66cba710 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,51 +1,29 @@ -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleDown, faArrowAltCircleRight, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faPhotoVideo, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; +import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast, BoolCast } from "../../fields/Types"; -import { emptyFunction, setupMoveUpEvents, Utils } from "../../Utils"; -import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; +import { Cast, NumCast } from "../../fields/Types"; +import { ImageField } from '../../fields/URLField'; +import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from '../documents/Documents'; -import { DragManager } from '../util/DragManager'; -import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView'; -import { ParentDocSelector } from './collections/ParentDocumentSelector'; -import './collections/ParentDocumentSelector.scss'; -import './PropertiesButtons.scss'; -import { MetadataEntryMenu } from './MetadataEntryMenu'; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentType } from '../documents/DocumentTypes'; +import { SelectionManager } from '../util/SelectionManager'; +import { undoBatch } from '../util/UndoManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { GoogleRef } from "./nodes/formattedText/FormattedTextBox"; -import { TemplateMenu } from "./TemplateMenu"; -import { Template, Templates } from "./Templates"; +import './PropertiesButtons.scss'; import React = require("react"); -import { Tooltip } from '@material-ui/core'; -import { SelectionManager } from '../util/SelectionManager'; -import SharingManager from '../util/SharingManager'; -import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; -import { ImageField } from '../../fields/URLField'; -import { undoBatch, UndoManager } from '../util/UndoManager'; -import { DocumentType } from '../documents/DocumentTypes'; -import { InkField } from '../../fields/InkField'; -import { PresBox } from './nodes/PresBox'; +import { CollectionViewType } from './collections/CollectionView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faLink); -library.add(faTag); -library.add(faTimes); -library.add(faArrowAltCircleDown); -library.add(faArrowAltCircleUp); -library.add(faArrowAltCircleRight); -library.add(faStopCircle); -library.add(faCheckCircle); -library.add(faCloudUploadAlt); -library.add(faSyncAlt); -library.add(faShare); -library.add(faPhotoVideo); const cloud: IconProp = "cloud-upload-alt"; const fetch: IconProp = "sync-alt"; @@ -58,7 +36,6 @@ enum UtilityButtonState { @observer export class PropertiesButtons extends React.Component<{}, {}> { - private _dragRef = React.createRef<HTMLDivElement>(); private _pullAnimating = false; private _pushAnimating = false; private _pullColorAnimating = false; @@ -76,13 +53,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { public static hasPulledHack = false; + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { if (SelectionManager.SelectedDocuments().length) { return SelectionManager.SelectedDocuments()[0]; - } else { return undefined; } + } else return undefined; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } @@ -167,11 +143,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-linker" style={{ backgroundColor: this.pullColor }} onPointerEnter={action(e => { - if (e.altKey) { - this.openHover = UtilityButtonState.OpenExternally; - } else if (e.shiftKey) { - this.openHover = UtilityButtonState.OpenRight; - } + e.altKey && (this.openHover = UtilityButtonState.OpenExternally); + e.shiftKey && (this.openHover = UtilityButtonState.OpenRight); })} onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} onClick={async e => { @@ -180,11 +153,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, UseCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } - CollectionDockingView.AddRightSplit(googleDoc); + CollectionDockingView.AddSplit(googleDoc, "right"); } else if (e.altKey) { e.preventDefault(); window.open(googleDocUrl); @@ -195,8 +168,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true); } }}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" - color="black" + <FontAwesomeIcon className="documentdecorations-icon" size="lg" color="black" style={{ WebkitAnimation: animation, MozAnimation: animation }} icon={(() => { switch (this.openHover) { @@ -212,248 +184,116 @@ export class PropertiesButtons extends React.Component<{}, {}> { </div> </Tooltip>; } + + @action @undoBatch + onLock = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.toggleLockPosition()); + } + @computed - get pinButton() { + get lockButton() { const targetDoc = this.selectedDoc; - const isPinned = targetDoc && Doc.isDocPinned(targetDoc); - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + return !targetDoc ? (null) : <Tooltip + title={<div className="dash-tooltip">{`${this.selectedDoc?.lockedPosition ? "Unlock" : "Lock"} " Position"`}</div>} placement="top"> <div> - <div className="propertiesButtons-linker" - style={{ backgroundColor: isPinned ? "white" : "", color: isPinned ? "black" : "white" }} - onClick={e => DockedFrameRenderer.PinDoc(targetDoc, isPinned)}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc.lockedPosition ? "on" : "off"}`} onPointerDown={this.onLock} > + <FontAwesomeIcon className="documentdecorations-icon" size="lg" + color={this.selectedDoc?.lockedPosition ? "black" : "white"} + icon={this.selectedDoc?.lockedPosition ? "unlock" : "lock"} /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: Doc.isDocPinned(targetDoc) ? "white" : "black", - // color: Doc.isDocPinned(targetDoc) ? "black" : "white" - // }} - >{Doc.isDocPinned(targetDoc) ? "Unpin" : "Pin"}</div> + >Position </div> </div> </Tooltip>; } @computed - get pinWithViewButton() { + get downloadButton() { const targetDoc = this.selectedDoc; - if (targetDoc) { - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - } - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with this view"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip + title={<div className="dash-tooltip">{"Download Document"}</div>} placement="top"> <div> - <div className="propertiesButtons-linker" - onClick={e => { - if (targetDoc) { - DockedFrameRenderer.PinDoc(targetDoc, false); - const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; - const x = targetDoc._panX; - const y = targetDoc._panY; - const scale = targetDoc._viewScale; - activeDoc.presPinView = true; - activeDoc.presPinViewX = x; - activeDoc.presPinViewY = y; - activeDoc.presPinViewScale = scale; - } - }}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> - <div style={{ position: 'relative', fontSize: 25, fontWeight: 700, transform: 'translate(0, -28px)', color: 'rgba(250,250,250,0.55)' }}>V</div> + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={() => this.selectedDoc && Doc.Zip(this.selectedDoc)}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="download" size="lg" /> </div> - - <div className="propertiesButtons-title">{"View"}</div> + <div className="propertiesButtons-title"> downld </div> </div> </Tooltip>; } - - @computed - get metadataButton() { - //const view0 = this.view0; - if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show metadata panel</div></>} placement="top"> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={<MetadataEntryMenu docs={[this.selectedDoc]} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}> - <div> - <div className={"propertiesButtons-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="lg" />} - </div> - <div className="propertiesButtons-title">Metadata</div> - </div> - </Flyout> - </div></Tooltip>; - } else { - return null; - } - - } - - @observable _aliasDown = false; - onAliasButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); - } @undoBatch - onAliasButtonMoved = () => { - if (this._dragRef.current) { - const dragDocView = this.selectedDocumentView!; - const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); - const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.dropAction = "alias"; - DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { - offsetX: dragData.offset[0], - offsetY: dragData.offset[1], - hideSource: false - }); - return true; - } - return false; - } - - @computed - get templateButton() { - const docView = this.selectedDocumentView; - const templates: Map<Template, boolean> = new Map(); - const views = [this.selectedDocumentView]; - Array.from(Object.values(Templates.TemplateList)).map(template => - templates.set(template, views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean))); - return !docView ? (null) : - <Tooltip title={<><div className="dash-tooltip">Customize layout</div></>} placement="top"> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} //onOpen={action(() => this._aliasDown = true)} onClose={action(() => this._aliasDown = false)} - content={<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}> - <div> - <div className={"propertiesButtons-linkButton-empty"} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="lg" />} - </div> - <div className="propertiesButtons-title">Layout</div> - </div> - </Flyout> - </div></Tooltip>; - } - - @undoBatch - onCopy = () => { - if (this.selectedDoc && this.selectedDocumentView) { - // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); - // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); - // copy.y = NumCast(this.selectedDoc.y) + 30; - // this.selectedDocumentView.props.addDocument?.(copy); - const alias = Doc.MakeAlias(this.selectedDoc); - alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); - alias.y = NumCast(this.selectedDoc.y) + 30; - this.selectedDocumentView.props.addDocument?.(alias); - } + @action + setDictation = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showAudio = dv.rootDoc._showAudio === !dv.rootDoc._showAudio); } @computed - get copyButton() { + get dictationButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Tap or Drag to create an alias"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Dictation Controls"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - ref={this._dragRef} - onPointerDown={this.onAliasButtonDown} - onClick={this.onCopy}> - {<FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showAudio ? "on" : "off"}`} onPointerDown={this.setDictation}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="microphone" size="lg" /> </div> - <div className="propertiesButtons-title">Alias</div> + <div className="propertiesButtons-title"> Dictate </div> </div> </Tooltip>; } - @action @undoBatch - onLock = () => { - this.selectedDocumentView?.toggleLockPosition(); + + @undoBatch + @action + setTitle = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showTitle = dv.rootDoc._showTitle === undefined ? "title" : undefined); } @computed - get lockButton() { + get titleButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?.lockedPosition ? - "Unlock Position" : "Lock Position"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Title Header"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "" }} - onPointerDown={this.onLock} > - {<FontAwesomeIcon className="documentdecorations-icon" - color={BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white"} - icon={BoolCast(this.selectedDoc?.lockedPosition) ? "unlock" : "lock"} size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showTitle ? "on" : "off"}`} onPointerDown={this.setTitle}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="text-width" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: BoolCast(this.selectedDoc?.lockedPosition) ? "white" : "black", - // color: BoolCast(this.selectedDoc?.lockedPosition) ? "black" : "white" - // }} - >Position </div> + <div className="propertiesButtons-title"> Title </div> </div> </Tooltip>; } - @computed - get downloadButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Download Document"}</div></>} placement="top"> - <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={async () => { - if (this.selectedDocumentView?.props.Document) { - Doc.Zip(this.selectedDocumentView?.props.Document); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="download" size="lg" />} - </div> - <div className="propertiesButtons-title"> downld </div> - </div> - </Tooltip>; + @undoBatch + @action + setCaption = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._showCaption = dv.rootDoc._showCaption === undefined ? "caption" : undefined); } @computed - get deleteButton() { + get captionButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">Close Document</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Caption Footer"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.deleteDocument}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="times" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showCaption ? "on" : "off"}`} onPointerDown={this.setCaption}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="closed-captioning" size="lg" /> </div> - <div className="propertiesButtons-title"> close </div> + <div className="propertiesButtons-title"> Caption </div> </div> </Tooltip>; } @undoBatch @action - deleteDocument = () => { - const selected = SelectionManager.SelectedDocuments().slice(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); - this.selectedDoc && (this.selectedDoc.deleted = true); - this.selectedDocumentView?.props.ContainingCollectionView?.removeDocument(this.selectedDocumentView?.props.Document); - SelectionManager.DeselectAll(); + setChrome = () => { + SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._chromeStatus = dv.rootDoc._chromeStatus === "disabled" ? "enabled" : "disabled"); } @computed - get sharingButton() { + get chromeButton() { const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Share Document"}</div></>} placement="top"> + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Editing UI"}</div>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); - } - }}> - {<FontAwesomeIcon className="propertiesButtons-icon" - icon="users" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._chromeStatus === "enabled" ? "on" : "off"}`} onPointerDown={this.setChrome}> + <FontAwesomeIcon className="propertiesButtons-icon" icon="edit" size="lg" /> </div> - <div className="propertiesButtons-title"> share </div> + <div className="propertiesButtons-title"> Controls </div> </div> </Tooltip>; } @@ -484,27 +324,31 @@ export class PropertiesButtons extends React.Component<{}, {}> { handleOptionChange = (e: any) => { const value = e.target.value; this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); - if (value === "nothing") { - this.selectedDocumentView?.noOnClick(); - } else if (value === "enterPortal") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.makeIntoPortal(); - } else if (value === "toggleDetail") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleDetail(); - } else if (value === "linkInPlace") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("inPlace", true, false); - } else if (value === "linkOnRight") { - this.selectedDocumentView?.noOnClick(); - this.selectedDocumentView?.toggleFollowLink("onRight", false, false); - } + + SelectionManager.SelectedDocuments().forEach(dv => { + if (value === "nothing") { + dv.noOnClick(); + } else if (value === "enterPortal") { + dv.noOnClick(); + dv.makeIntoPortal(); + } else if (value === "toggleDetail") { + dv.noOnClick(); + dv.toggleDetail(); + } else if (value === "linkInPlace") { + dv.noOnClick(); + dv.toggleFollowLink("inPlace", true, false); + } else if (value === "linkOnRight") { + dv.noOnClick(); + dv.toggleFollowLink("add:right", false, false); + } + }); } @undoBatch @action editOnClickScript = () => { if (this.selectedDoc) { - DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => DocUtils.makeCustomViewClicked(dv.rootDoc, undefined, "onClick")); + else DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); } } @@ -563,13 +407,8 @@ export class PropertiesButtons extends React.Component<{}, {}> { title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>} placement="top"> <div> <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={() => { - if (this.selectedDocumentView) { - GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDocumentView.Document }).then(console.log); - } - }}> - {<FontAwesomeIcon className="documentdecorations-icon" - icon="cloud-upload-alt" size="lg" />} + onPointerDown={() => this.selectedDoc && GooglePhotos.Export.CollectionToAlbum({ collection: this.selectedDoc }).then(console.log)}> + {<FontAwesomeIcon className="documentdecorations-icon" icon="cloud-upload-alt" size="lg" />} </div> <div className="propertiesButtons-title"> google </div> </div> @@ -580,33 +419,30 @@ export class PropertiesButtons extends React.Component<{}, {}> { get clustersButton() { const targetDoc = this.selectedDoc; return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?.useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>} placement="top"> + title={<><div className="dash-tooltip">{this.selectedDoc?._useClusters ? "Stop Showing Clusters" : "Show Clusters"}</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?.useClusters ? "white" : "" }} - onPointerDown={this.changeClusters}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?.useClusters ? "black" : "white"} - icon="braille" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._useClusters ? "on" : "off"}`} onPointerDown={this.changeClusters}> + <FontAwesomeIcon className="documentdecorations-icon" icon="braille" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: this.selectedDoc?.useClusters ? "white" : "black", - // color: this.selectedDoc?.useClusters ? "black" : "white" - // }} - > clusters </div> + <div className="propertiesButtons-title" > clusters </div> </div> </Tooltip>; } @action @undoBatch changeFitToBox = () => { - this.selectedDoc && (this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox); + if (this.selectedDoc) { + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._fitToBox = !dv.rootDoc._fitToBox); + else this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox; + } } @action @undoBatch changeClusters = () => { - this.selectedDoc && (this.selectedDoc.useClusters = !this.selectedDoc.useClusters); + if (this.selectedDoc) { + if (SelectionManager.SelectedDocuments().length) SelectionManager.SelectedDocuments().forEach(dv => dv.rootDoc._useClusters = !dv.rootDoc._useClusters); + else this.selectedDoc._useClusters = !this.selectedDoc._useClusters; + } } @computed @@ -615,19 +451,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{this.selectedDoc?._fitToBox ? "Stop Fitting Content" : "Fit Content"}</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - style={{ backgroundColor: this.selectedDoc?._fitToBox ? "white" : "" }} - onPointerDown={this.changeFitToBox}> - {<FontAwesomeIcon className="documentdecorations-icon" - color={this.selectedDoc?._fitToBox ? "black" : "white"} - icon="expand" size="lg" />} + <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._fitToBox ? "on" : "off"}`} onPointerDown={this.changeFitToBox}> + <FontAwesomeIcon className="documentdecorations-icon" icon="expand" size="lg" /> </div> - <div className="propertiesButtons-title" - // style={{ - // backgroundColor: this.selectedDoc?._fitToBox ? "white" : "black", - // color: this.selectedDoc?._fitToBox ? "black" : "white" - // }} - > {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> + <div className="propertiesButtons-title"> {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> </div> </Tooltip>; } @@ -650,101 +477,45 @@ export class PropertiesButtons extends React.Component<{}, {}> { return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">Make Mask</div></>} placement="top"> <div> - <div className={"propertiesButtons-linkButton-empty"} - onPointerDown={this.makeMask}> - {<FontAwesomeIcon className="documentdecorations-icon" - color="white" icon="paint-brush" size="lg" />} + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={this.makeMask}> + <FontAwesomeIcon className="documentdecorations-icon" color="white" icon="paint-brush" size="lg" /> </div> <div className="propertiesButtons-title"> mask </div> </div> </Tooltip>; } - @computed - get contextButton() { - if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Show Context</div></>} placement="top"> - <div> - <div className={"propertiesButtons-linkButton-empty"}> - <ParentDocSelector Document={this.selectedDoc} addDocTab={(doc, where) => { - where === "onRight" ? CollectionDockingView.AddRightSplit(doc) : - this.selectedDocumentView?.props.addDocTab(doc, "onRight"); - return true; - }} /> - </div> - <div className="propertiesButtons-title"> context </div> - </div> - </Tooltip>; - } else { - return false; - } - - } - - // @computed - // get importButton() { - // const targetDoc = this.selectedDoc; - // return !targetDoc ? (null) : <Tooltip - // title={<><div className="dash-tooltip">{"Import a Document"}</div></>}> - // <div className={"propertiesButtons-linkButton-empty"} - // onPointerDown={() => { - // if (this.selectedDocumentView) { - // CollectionFreeFormView.importDocument(100, 100); - // } - // }}> - // {<FontAwesomeIcon className="documentdecorations-icon" - // icon="upload" size="sm" />} - // </div> - // </Tooltip>; - // } - - render() { if (!this.selectedDoc) return (null); - const isText = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + const layoutField = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)]; + const isText = layoutField instanceof RichTextField; + const isImage = layoutField instanceof ImageField; + const isInk = layoutField instanceof InkField; + const isCollection = this.selectedDoc.type === DocumentType.COL; + const isFreeForm = this.selectedDoc._viewType === CollectionViewType.Freeform; const considerPull = isText && this.considerGoogleDocsPull; const considerPush = isText && this.considerGoogleDocsPush; - const isImage = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof ImageField; - const isInk = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof InkField; - const isCollection = this.selectedDoc.type === DocumentType.COL ? true : false; - const isFreeForm = this.selectedDoc._viewType === "freeform" ? true : false; - const hasContext = this.selectedDoc.context ? true : false; - return <div><div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> + return <div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> <div className="propertiesButtons-button"> - {this.templateButton} + {this.titleButton} </div> - {/* <div className="propertiesButtons-button"> - {this.metadataButton} - </div> */} <div className="propertiesButtons-button"> - {this.pinButton} + {this.captionButton} </div> - <div className="propertiesButtons-button"> - {this.pinWithViewButton} - </div> - <div className="propertiesButtons-button" style={{ display: hasContext ? "" : "none" }}> - {this.copyButton} + <div className="propertiesButtons-button" style={{ display: isCollection ? "" : "none" }}> + {this.chromeButton} </div> <div className="propertiesButtons-button"> {this.lockButton} </div> - <div className="propertiesButtons-button"> - {this.downloadButton} - </div> - <div className="propertiesButtons-button"> - {this.deleteButton} + <div className="propertiesButtons-button" style={{ display: isText || isImage ? "" : "none" }}> + {this.dictationButton} </div> <div className="propertiesButtons-button"> {this.onClickButton} </div> - <div className="propertiesButtons-button"> - {this.sharingButton} - </div> - <div className="propertiesButtons-button"> - {this.contextButton} - </div> <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div> @@ -754,22 +525,16 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}> {this.googlePhotosButton} </div> - {/* <div className="propertiesButtons-button" style={{ display: !isCollection ? "none" : "" }}> - {this.importButton} - </div> */} <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> {this.clustersButton} </div> - <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> {this.fitContentButton} </div> - <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> {this.maskButton} </div> - </div> </div>; } } diff --git a/src/client/views/PropertiesDocContextSelector.scss b/src/client/views/PropertiesDocContextSelector.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/client/views/PropertiesDocContextSelector.scss diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx new file mode 100644 index 000000000..427748fe7 --- /dev/null +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -0,0 +1,58 @@ +import { IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { Doc } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { NumCast, StrCast } from "../../fields/Types"; +import { CollectionViewType } from "./collections/CollectionView"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import './PropertiesDocContextSelector.scss'; +import { SearchUtil } from "../util/SearchUtil"; + +type PropertiesDocContextSelectorProps = { + Document: Doc, + Stack?: any, + hideTitle?: boolean, + addDocTab(doc: Doc, location: string): void +}; + +@observer +export class PropertiesDocContextSelector extends React.Component<PropertiesDocContextSelectorProps> { + @observable private _docs: { col: Doc, target: Doc }[] = []; + @observable private _otherDocs: { col: Doc, target: Doc }[] = []; + _reaction: IReactionDisposer | undefined; + + componentDidMount() { this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); } + componentWillUnmount() { this._reaction?.(); } + async fetchDocuments() { + const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); + const containerProtoSets = await Promise.all(aliases.map(async alias => ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); + const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); + const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => SearchUtil.GetAliasesOfDocument(container))); + const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); + const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => SearchUtil.GetAliasesOfDocument(dp))); + const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); + runInAction(() => { + this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).filter(doc => !Doc.IsSystem(doc)).map(doc => ({ col: doc, target: this.props.Document })); + this._otherDocs = []; + }); + } + + getOnClick = (col: Doc, target: Doc) => { + col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; + if (col._viewType === CollectionViewType.Freeform) { + col._panX = NumCast(target.x) + NumCast(target._width) / 2; + col._panY = NumCast(target.y) + NumCast(target._height) / 2; + } + this.props.addDocTab(col, "add:right"); + } + + render() { + return <div> + {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>} + {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={() => this.getOnClick(doc.col, doc.target)}>{StrCast(doc.col.title)}</a></p>)} + {this._otherDocs.length ? <hr key="hr" /> : null} + {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={() => this.getOnClick(doc.col, doc.target)}>{StrCast(doc.col.title)}</a></p>)} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/PropertiesView.scss index aee28366a..e5f9e0417 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -6,7 +6,7 @@ cursor: auto; overflow-x: hidden; - overflow-y: scroll; + overflow-y: auto; .propertiesView-title { background-color: rgb(159, 159, 159); @@ -47,7 +47,7 @@ } .propertiesView-settings { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; font-size: 12.5px; font-weight: bold; @@ -87,7 +87,7 @@ } .propertiesView-sharing { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-sharing-title { @@ -121,6 +121,19 @@ padding: 10px; margin-left: 5px; + .propertiesView-acls-checkbox { + float: right; + height: 20px; + margin-top: -20px; + margin-right: -15; + + .propertiesView-acls-checkbox-text { + font-size: 7px; + margin-top: -10px; + margin-left: 6px; + } + } + .change-buttons { display: flex; @@ -137,7 +150,7 @@ } .propertiesView-appearance { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-appearance-title { @@ -174,7 +187,7 @@ } .propertiesView-transform { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-transform-title { @@ -258,7 +271,8 @@ // display: inline-table; background-color: #ececec; max-height: 130px; - overflow-y: scroll; + overflow-y: auto; + width: 92%; .propertiesView-sharingTable-item { @@ -267,7 +281,6 @@ padding: 3px; align-items: center; border-bottom: 0.5px solid grey; - cursor: pointer; &:hover .propertiesView-sharingTable-item-name { overflow-x: unset; @@ -290,7 +303,6 @@ margin-left: auto; .permissions-select { - z-index: 1; border: none; background-color: inherit; width: 75px; @@ -310,7 +322,7 @@ } .propertiesView-fields { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-fields-title { @@ -383,6 +395,41 @@ } } + .propertiesView-contexts { + + .propertiesView-contexts-title { + font-weight: bold; + font-size: 12.5px; + padding: 4px; + display: flex; + color: white; + padding-left: 8px; + background-color: rgb(51, 51, 51); + + &:hover { + cursor: pointer; + } + + .propertiesView-contexts-title-icon { + float: right; + justify-items: right; + align-items: flex-end; + margin-left: auto; + margin-right: 9px; + + &:hover { + cursor: pointer; + } + } + } + + .propertiesView-contexts-content { + overflow: hidden; + padding: 10px; + } + + } + .propertiesView-layout { .propertiesView-layout-title { @@ -419,7 +466,7 @@ } .propertiesView-presTrails { - border-bottom: 1px solid black; + //border-bottom: 1px solid black; //padding: 8.5px; .propertiesView-presTrails-title { @@ -718,7 +765,9 @@ border: none; padding: 6px; padding-bottom: 2px; - + background: #eeeeee; + border-top: 1px solid; + border-left: 1px solid; &:hover { border: 0.75px solid rgb(122, 28, 28); diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 57e968aa7..46f38795c 100644 --- a/src/client/views/collections/collectionFreeForm/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -1,46 +1,40 @@ import React = require("react"); -import { observer } from "mobx-react"; -import "./PropertiesView.scss"; -import { observable, action, computed, runInAction } from "mobx"; -import { Doc, Field, WidthSym, HeightSym, AclSym, AclPrivate, AclReadonly, AclAddonly, AclEdit, AclAdmin, Opt, DocCastAsync } from "../../../../fields/Doc"; -import { ComputedField } from "../../../../fields/ScriptField"; -import { EditableView } from "../../EditableView"; -import { KeyValueBox } from "../../nodes/KeyValueBox"; -import { Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { ContentFittingDocumentView } from "../../nodes/ContentFittingDocumentView"; -import { returnFalse, returnOne, emptyFunction, emptyPath, returnTrue, returnZero, returnEmptyFilter, Utils } from "../../../../Utils"; -import { Id } from "../../../../fields/FieldSymbols"; -import { Transform } from "../../../util/Transform"; -import { PropertiesButtons } from "../../PropertiesButtons"; -import { SelectionManager } from "../../../util/SelectionManager"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip, Checkbox } from "@material-ui/core"; -import SharingManager from "../../../util/SharingManager"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SharingPermissions, GetEffectiveAcl } from "../../../../fields/util"; -import { InkField } from "../../../../fields/InkField"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { Checkbox, Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; import { ColorState, SketchPicker } from "react-color"; -import "./FormatShapePane.scss"; -import { PresBox } from "../../nodes/PresBox"; -import { DocumentManager } from "../../../util/DocumentManager"; -import FormatShapePane from "./FormatShapePane"; +import { AclAddonly, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, Field, HeightSym, WidthSym } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { InkField } from "../../fields/InkField"; +import { ComputedField } from "../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../fields/Types"; +import { GetEffectiveAcl, SharingPermissions } from "../../fields/util"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../Utils"; +import { DocumentType } from "../documents/DocumentTypes"; +import { DocumentManager } from "../util/DocumentManager"; +import { SelectionManager } from "../util/SelectionManager"; +import { SharingManager } from "../util/SharingManager"; +import { Transform } from "../util/Transform"; +import { undoBatch, UndoManager } from "../util/UndoManager"; +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { EditableView } from "./EditableView"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { ContentFittingDocumentView } from "./nodes/ContentFittingDocumentView"; +import { KeyValueBox } from "./nodes/KeyValueBox"; +import { PresBox } from "./nodes/PresBox"; +import { PropertiesButtons } from "./PropertiesButtons"; +import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector"; +import "./PropertiesView.scss"; +import { intersection } from "lodash"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; const _global = (window /* browser */ || global /* node */) as any; -// import * as fa from '@fortawesome/free-solid-svg-icons'; -// import { library } from "@fortawesome/fontawesome-svg-core"; - -// library.add(fa.faPlus, fa.faMinus, fa.faCog); - interface PropertiesViewProps { width: number; height: number; - renderDepth: number; - ScreenToLocalTransform: () => Transform; - onDown: (event: any) => void; } @observer @@ -49,30 +43,30 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get MAX_EMBED_HEIGHT() { return 200; } + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } @computed get selectedDocumentView() { - if (SelectionManager.SelectedDocuments().length) { - return SelectionManager.SelectedDocuments()[0]; - } else if (PresBox.Instance && PresBox.Instance._selectedArray.length) { - return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); - } else { return undefined; } + if (SelectionManager.SelectedDocuments().length) return SelectionManager.SelectedDocuments()[0]; + if (PresBox.Instance && PresBox.Instance._selectedArray.length) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc); + return undefined; } @computed get isPres(): boolean { if (this.selectedDoc?.type === DocumentType.PRES) return true; return false; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get dataDoc() { return this.selectedDocumentView?.dataDoc; } + @computed get dataDoc() { return this.selectedDoc?.[DataSym]; } @observable layoutFields: boolean = false; - @observable openActions: boolean = true; + @observable openOptions: boolean = true; @observable openSharing: boolean = true; @observable openFields: boolean = true; @observable openLayout: boolean = true; + @observable openContexts: boolean = true; @observable openAppearance: boolean = true; @observable openTransform: boolean = true; // @observable selectedUser: string = ""; // @observable addButtonPressed: boolean = false; + @observable layoutDocAcls: boolean = false; //Pres Trails booleans: @observable openPresTransitions: boolean = false; @@ -80,7 +74,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; - @observable inActions: boolean = false; + @observable inOptions: boolean = false; @observable _controlBtn: boolean = false; @observable _lock: boolean = false; @@ -135,24 +129,26 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get expandedField() { if (this.dataDoc && this.selectedDoc) { const ids: { [key: string]: string } = {}; - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : + SelectionManager.SelectedDocuments().map(dv => this.layoutFields ? Doc.Layout(dv.layoutDoc) : dv.dataDoc); + docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; for (const key of Object.keys(ids).slice().sort()) { - const contents = doc[key]; + const docvals = new Set<any>(); + docs.forEach(doc => docvals.add(doc[key])); + const contents = Array.from(docvals.keys()).length > 1 ? "-multiple" : docs[0][key]; if (key[0] === "#") { rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "2px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> </div>); } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" + const contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} height={13} fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} + GetValue={() => contents !== undefined ? Field.toString(contents as Field) : "null"} + SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} />; rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> @@ -175,45 +171,45 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get noviceFields() { - if (this.dataDoc && this.selectedDoc) { + if (this.dataDoc) { const ids: { [key: string]: string } = {}; - const doc = this.dataDoc; - doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.dataDoc] : SelectionManager.SelectedDocuments().map(dv => dv.dataDoc); + docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key))); const rows: JSX.Element[] = []; - for (const key of Object.keys(ids).slice().sort()) { - if ((key[0] === key[0].toUpperCase() && key.substring(0, 3) !== "ACL" && key !== "UseCors") - || key[0] === "#" || key === "author" || - key === "creationDate" || key.indexOf("lastModified") !== -1) { - - const contents = doc[key]; - if (key[0] === "#") { + const noviceReqFields = ["author", "creationDate", "tags"]; + const noviceLayoutFields = ["_curPage"]; + const noviceKeys = [...Array.from(Object.keys(ids)).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("ACL"))), + ...noviceReqFields, ...noviceLayoutFields]; + for (const key of noviceKeys.sort()) { + const docvals = new Set<any>(); + docs.forEach(doc => docvals.add(doc[key])); + const contents = Array.from(docvals.keys()).length > 1 ? "-multiple" : docs[0][key]; + if (key[0] === "#") { + rows.push(<div className="uneditable-field" key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> + + </div>); + } else if (contents !== undefined) { + const value = Field.toString(contents as Field); + if (noviceReqFields.includes(key) || key.indexOf("lastModified") !== -1) { rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key}</span> - - </div>); + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> + <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> + </div>); } else { - const value = Field.toString(contents as Field); - if (key === "author" || key === "creationDate" || key.indexOf("lastModified") !== -1) { - rows.push(<div className="uneditable-field" key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ": "}</span> - <div style={{ whiteSpace: "nowrap", overflowX: "hidden" }}>{value}</div> - </div>); - } else { - let contentElement: (JSX.Element | null)[] | JSX.Element = []; - contentElement = <EditableView key="editableView" - contents={contents !== undefined ? Field.toString(contents as Field) : "null"} - height={13} - fontSize={10} - GetValue={() => Field.toKeyValueString(doc, key)} - SetValue={(value: string) => KeyValueBox.SetField(doc, key, value, true)} - />; - - rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> - <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> - - {contentElement} - </div>); - } + const contentElement = <EditableView key="editableView" + contents={value} + height={13} + fontSize={10} + GetValue={() => contents !== undefined ? Field.toString(contents as Field) : "null"} + SetValue={(value: string) => { docs.map(doc => KeyValueBox.SetField(doc, key, value, true)); return true; }} + />; + + rows.push(<div style={{ display: "flex", overflowY: "visible", marginBottom: "-1px" }} key={key}> + <span style={{ fontWeight: "bold", whiteSpace: "nowrap" }}>{key + ":"}</span> + + {contentElement} + </div>); } } } @@ -232,18 +228,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @undoBatch setKeyValue = (value: string) => { - if (this.selectedDoc && this.dataDoc) { - const doc = this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc; + const docs = SelectionManager.SelectedDocuments().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.SelectedDocuments().map(dv => this.layoutFields ? dv.layoutDoc : dv.dataDoc); + docs.forEach(doc => { if (value.indexOf(":") !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + const splits = newVal.split(":"); + KeyValueBox.SetField(doc, splits[0], splits[1], true); + const tags = StrCast(doc.tags, ":"); + if (tags.includes(`${splits[0]}:`) && splits[1] === "undefined") { + KeyValueBox.SetField(doc, "tags", `"${tags.replace(splits[0] + ":", "")}"`, true); + } return true; } else if (value[0] === "#") { const newVal = value + `:'${value}'`; - KeyValueBox.SetField(doc, newVal.substring(0, newVal.indexOf(":")), newVal.substring(newVal.indexOf(":") + 1, newVal.length), true); + doc[DataSym][value] = value; + const tags = StrCast(doc.tags, ":"); + if (!tags.includes(`${value}:`)) { + doc[DataSym].tags = `${tags + value + ':'}`; + } return true; } - } + }); return false; } @@ -257,8 +262,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ref && observer.observe(ref); } + @computed get contexts() { + return !this.selectedDoc ? (null) : <PropertiesDocContextSelector Document={this.selectedDoc} hideTitle={true} addDocTab={(doc, where) => CollectionDockingView.AddSplit(doc, "right")} />; + } + previewBackground = () => "lightgrey"; @computed get layoutPreview() { + if (SelectionManager.SelectedDocuments().length > 1) { + return "-- multiple selected --"; + } if (this.selectedDoc) { const layoutDoc = Doc.Layout(this.selectedDoc); const panelHeight = StrCast(Doc.LayoutField(layoutDoc)).includes("FormattedTextBox") ? this.rtfHeight : this.docHeight; @@ -268,7 +280,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { Document={layoutDoc} DataDoc={this.dataDoc} LibraryPath={emptyPath} - renderDepth={this.props.renderDepth + 1} + renderDepth={1} rootSelected={returnFalse} treeViewDoc={undefined} backgroundColor={this.previewBackground} @@ -283,6 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { focus={returnFalse} ScreenToLocalTransform={this.getTransform} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} addDocument={returnFalse} @@ -308,19 +321,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, this.selectedDoc!); + const docs = SelectionManager.SelectedDocuments().length < 2 ? [this.selectedDoc!] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document); + SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); } /** * @returns the options for the permissions dropdown. */ getPermissionsSelect(user: string, permission: string) { + const dropdownValues: string[] = Object.values(SharingPermissions); + if (permission === "-multiple-") dropdownValues.unshift(permission); return <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}> - {Object.values(SharingPermissions).map(permission => { + {dropdownValues.map(permission => { return ( - <option key={permission} value={permission} selected={this.selectedDoc![`ACL-${user.replace(".", "_")}`] === permission}> + <option key={permission} value={permission}> {permission} </option>); })} @@ -344,8 +360,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get expansionIcon() { return <Tooltip title={<div className="dash-tooltip">{"Show more permissions"}</div>}> <div className="expansion-button" onPointerDown={() => { - if (this.selectedDocumentView) { - SharingManager.Instance.open(this.selectedDocumentView); + if (this.selectedDocumentView || this.selectedDoc) { + SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDocumentView ? this.selectedDocumentView : undefined, this.selectedDoc); } }}> <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" /> @@ -356,7 +372,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { /** * @returns a row of the permissions panel */ - sharingItem(name: string, effectiveAcl: symbol, permission: string) { + sharingItem(name: string, admin: boolean, permission: string) { return <div className="propertiesView-sharingTable-item" key={name + permission} // style={{ backgroundColor: this.selectedUser === name ? "#bcecfc" : "" }} // onPointerDown={action(() => this.selectedUser = this.selectedUser === name ? "" : name)} @@ -364,7 +380,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-sharingTable-item-name" style={{ width: name !== "Me" ? "85px" : "80px" }}> {name} </div> {/* {name !== "Me" ? this.notifyIcon : null} */} <div className="propertiesView-sharingTable-item-permission"> - {effectiveAcl === AclAdmin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} + {admin && permission !== "Owner" ? this.getPermissionsSelect(name, permission) : permission} {permission === "Owner" ? this.expansionIcon : null} </div> </div>; @@ -382,15 +398,29 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { [AclAdmin, SharingPermissions.Admin] ]); - const effectiveAcl = GetEffectiveAcl(this.selectedDoc!); + // all selected docs + const docs = SelectionManager.SelectedDocuments().length < 2 ? + [this.layoutDocAcls ? this.selectedDoc! : this.selectedDoc![DataSym]] + : SelectionManager.SelectedDocuments().map(docView => this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym]); + + const target = docs[0]; + + // tslint:disable-next-line: no-unnecessary-callback-wrapper + const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); + const showAdmin = effectiveAcls.every(acl => acl === AclAdmin); + + // users in common between all docs + const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))); + const tableEntries = []; // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { - if (this.selectedDoc![AclSym]) { - for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { + if (commonKeys.length) { + for (const key of commonKeys) { const name = key.substring(4).replace("_", "."); - if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author/* && sidebarUsersDisplayed![name] !== false*/) { - tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]); + if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public"/* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : "-multiple-")); } } } @@ -402,9 +432,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { // } // }) - // shifts the current user and the owner to the top of the doc. - tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); - if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.author), effectiveAcl, "Owner")); + // shifts the current user, owner, public to the top of the doc. + tableEntries.unshift(this.sharingItem("Public", showAdmin, docs.every(doc => doc["ACL-Public"] === docs[0]["ACL-Public"]) ? (AclMap.get(target[AclSym]?.["ACL-Public"]) || SharingPermissions.None) : "-multiple-")); + tableEntries.unshift(this.sharingItem("Me", showAdmin, docs.every(doc => doc.author === Doc.CurrentUserEmail) ? "Owner" : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : "-multiple-")); + if (Doc.CurrentUserEmail !== target.author && docs.every(doc => doc.author === docs[0].author)) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, "Owner")); return <div className="propertiesView-sharingTable"> {tableEntries} @@ -425,21 +456,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get editableTitle() { + const titles = new Set<string>(); + SelectionManager.SelectedDocuments().forEach(dv => titles.add(StrCast(dv.rootDoc.title))); + const title = Array.from(titles.keys()).length > 1 ? "--multiple selected--" : StrCast(this.selectedDoc?.title); return <div className="editable-title"><EditableView key="editableView" - contents={StrCast(this.selectedDoc?.title)} + contents={title} height={25} fontSize={14} - GetValue={() => StrCast(this.selectedDoc?.title)} + GetValue={() => title} SetValue={this.setTitle} /> </div>; } @undoBatch @action setTitle = (value: string) => { - if (this.dataDoc) { - this.selectedDoc && (this.selectedDoc.title = value); - KeyValueBox.SetField(this.dataDoc, "title", value, true); + if (SelectionManager.SelectedDocuments().length > 1) { + SelectionManager.SelectedDocuments().map(dv => Doc.SetInPlace(dv.rootDoc, "title", value, true)); + return true; + } else if (this.dataDoc) { + if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, "title", value, true); + else KeyValueBox.SetField(this.dataDoc, "title", value, true); return true; } return false; @@ -496,15 +533,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @computed get controlPointsButton() { - return <div className="inking-button"> + const formatInstance = InkStrokeProperties.Instance; + return !formatInstance ? (null) : <div className="inking-button"> <Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}> - <div className="inking-button-points" onPointerDown={action(() => FormatShapePane.Instance._controlBtn = !FormatShapePane.Instance._controlBtn)} style={{ backgroundColor: FormatShapePane.Instance._controlBtn ? "black" : "" }}> + <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlBtn = !formatInstance._controlBtn)} style={{ backgroundColor: formatInstance._controlBtn ? "black" : "" }}> <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" /> </div> </Tooltip> - <Tooltip title={<div className="dash-tooltip">{FormatShapePane.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> - <div className="inking-button-lock" onPointerDown={action(() => FormatShapePane.Instance._lock = !FormatShapePane.Instance._lock)} > - <FontAwesomeIcon icon={FormatShapePane.Instance._lock ? "lock" : "unlock"} color="white" size="lg" /> + <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}> + <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} > + <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" /> </div> </Tooltip> <Tooltip title={<div className="dash-tooltip">{"Rotate 90Ëš"}</div>}> @@ -524,7 +562,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="inputBox-title"> {title} </div> <input className="inputBox-input" type="text" value={value} - onChange={e => setter(e.target.value)} /> + onChange={e => { + setter(e.target.value); + }} + onKeyPress={e => { + e.stopPropagation(); + }} /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} > @@ -559,7 +602,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oldX = NumCast(this.selectedDoc?.x); const oldY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height))); const doc = this.selectedDoc; if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { const ink = Cast(doc.data, InkField)?.inkData; @@ -581,7 +624,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const oX = NumCast(this.selectedDoc?.x); const oY = NumCast(this.selectedDoc?.y); this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width))); const docu = this.selectedDoc; if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { const ink = Cast(docu.data, InkField)?.inkData; @@ -619,12 +662,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { set shapeWid(value) { const oldWidth = NumCast(this.selectedDoc?._width); this.selectedDoc && (this.selectedDoc._width = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth); } set shapeHgt(value) { const oldHeight = NumCast(this.selectedDoc?._height); this.selectedDoc && (this.selectedDoc._height = Number(value)); - FormatShapePane.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); + InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight); } @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); } @@ -829,35 +872,33 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { if (this.selectedDoc && !this.isPres) { return <div className="propertiesView" style={{ width: this.props.width, + minWidth: this.props.width //overflowY: this.scrolling ? "scroll" : "visible" }} > <div className="propertiesView-title" style={{ width: this.props.width }}> Properties - {/* <div className="propertiesView-title-icon" onPointerDown={this.props.onDown}> - <FontAwesomeIcon icon="times" color="black" size="sm" /> - </div> */} </div> <div className="propertiesView-name"> {this.editableTitle} </div> - <div className="propertiesView-settings" onPointerEnter={() => runInAction(() => { this.inActions = true; })} - onPointerLeave={action(() => this.inActions = false)}> + <div className="propertiesView-settings" onPointerEnter={action(() => this.inOptions = true)} + onPointerLeave={action(() => this.inOptions = false)}> <div className="propertiesView-settings-title" - onPointerDown={() => runInAction(() => { this.openActions = !this.openActions; })} - style={{ backgroundColor: this.openActions ? "black" : "" }}> - Actions + onPointerDown={action(() => this.openOptions = !this.openOptions)} + style={{ backgroundColor: this.openOptions ? "black" : "" }}> + Options <div className="propertiesView-settings-title-icon"> - <FontAwesomeIcon icon={this.openActions ? "caret-down" : "caret-right"} size="lg" color="white" /> + <FontAwesomeIcon icon={this.openOptions ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> - {!this.openActions ? (null) : + {!this.openOptions ? (null) : <div className="propertiesView-settings-content"> <PropertiesButtons /> </div>} </div> <div className="propertiesView-sharing"> <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + onPointerDown={action(() => this.openSharing = !this.openSharing)} style={{ backgroundColor: this.openSharing ? "black" : "" }}> Sharing {"&"} Permissions <div className="propertiesView-sharing-title-icon"> @@ -903,7 +944,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {!this.isInk ? (null) : <div className="propertiesView-appearance"> <div className="propertiesView-appearance-title" - onPointerDown={() => runInAction(() => { this.openAppearance = !this.openAppearance; })} + onPointerDown={action(() => this.openAppearance = !this.openAppearance)} style={{ backgroundColor: this.openAppearance ? "black" : "" }}> Appearance <div className="propertiesView-appearance-title-icon"> @@ -918,7 +959,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.isInk ? <div className="propertiesView-transform"> <div className="propertiesView-transform-title" - onPointerDown={() => runInAction(() => { this.openTransform = !this.openTransform; })} + onPointerDown={action(() => this.openTransform = !this.openTransform)} style={{ backgroundColor: this.openTransform ? "black" : "" }}> Transform <div className="propertiesView-transform-title-icon"> @@ -932,7 +973,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-fields"> <div className="propertiesView-fields-title" - onPointerDown={() => runInAction(() => { this.openFields = !this.openFields; })} + onPointerDown={action(() => this.openFields = !this.openFields)} style={{ backgroundColor: this.openFields ? "black" : "" }}> Fields {"&"} Tags <div className="propertiesView-fields-title-icon"> @@ -948,12 +989,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {novice ? this.noviceFields : this.expandedField} </div>} </div> + <div className="propertiesView-contexts"> + <div className="propertiesView-contexts-title" + onPointerDown={action(() => this.openContexts = !this.openContexts)} + style={{ backgroundColor: this.openContexts ? "black" : "" }}> + Contexts + <div className="propertiesView-contexts-title-icon"> + <FontAwesomeIcon icon={this.openContexts ? "caret-down" : "caret-right"} size="lg" color="white" /> + </div> + </div> + {this.openContexts ? <div className="propertiesView-contexts-content" >{this.contexts}</div> : null} + </div> <div className="propertiesView-layout"> <div className="propertiesView-layout-title" - onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })} + onPointerDown={action(() => this.openLayout = !this.openLayout)} style={{ backgroundColor: this.openLayout ? "black" : "" }}> Layout - <div className="propertiesView-layout-title-icon" onPointerDown={() => runInAction(() => { this.openLayout = !this.openLayout; })}> + <div className="propertiesView-layout-title-icon"> <FontAwesomeIcon icon={this.openLayout ? "caret-down" : "caret-right"} size="lg" color="white" /> </div> </div> @@ -991,7 +1043,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>} {!selectedItem ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openPresProgressivize = !this.openPresProgressivize; })} + onPointerDown={action(() => { this.openPresProgressivize = !this.openPresProgressivize; })} style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}> <FontAwesomeIcon icon={"tasks"} /> Progressivize <div className="propertiesView-presTrails-title-icon"> @@ -1004,7 +1056,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>} {!selectedItem ? (null) : <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openSlideOptions = !this.openSlideOptions; })} + onPointerDown={action(() => { this.openSlideOptions = !this.openSlideOptions; })} style={{ backgroundColor: this.openSlideOptions ? "black" : "" }}> <FontAwesomeIcon icon={"cog"} /> {PresBox.Instance.stringType} options <div className="propertiesView-presTrails-title-icon"> @@ -1017,7 +1069,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div>} <div className="propertiesView-presTrails"> <div className="propertiesView-presTrails-title" - onPointerDown={() => runInAction(() => { this.openAddSlide = !this.openAddSlide; })} + onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> <FontAwesomeIcon icon={"plus"} /> Add new slide <div className="propertiesView-presTrails-title-icon"> @@ -1030,7 +1082,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> {/* <div className="propertiesView-sharing"> <div className="propertiesView-sharing-title" - onPointerDown={() => runInAction(() => { this.openSharing = !this.openSharing; })} + onPointerDown={acition(() => { this.openSharing = !this.openSharing; })} style={{ backgroundColor: this.openSharing ? "black" : "" }}> Sharing {"&"} Permissions <div className="propertiesView-sharing-title-icon"> diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index db087fb23..c598b2861 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -1,17 +1,12 @@ -import * as React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; -import './ScriptingRepl.scss'; -import { Scripting, CompileScript, ts, Transformer } from '../util/Scripting'; +import * as React from 'react'; import { DocumentManager } from '../util/DocumentManager'; -import { OverlayView } from './OverlayView'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { CompileScript, Scripting, Transformer, ts } from '../util/Scripting'; import { DocumentIconContainer } from './nodes/DocumentIcon'; - -library.add(faCaretDown); -library.add(faCaretRight); +import { OverlayView } from './OverlayView'; +import './ScriptingRepl.scss'; @observer export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index eb20fc257..870af03aa 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -10,7 +10,7 @@ import { Doc, DocListCast } from "../../fields/Doc"; import { Docs, DocUtils, } from "../documents/Documents"; import { StrCast, Cast } from "../../fields/Types"; import { CollectionTreeView } from "./collections/CollectionTreeView"; -import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter } from "../../Utils"; +import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath, returnZero, returnEmptyFilter, returnEmptyDoclist } from "../../Utils"; import { Transform } from "../util/Transform"; import { ScriptField, ComputedField } from "../../fields/ScriptField"; import { Scripting } from "../util/Scripting"; @@ -116,9 +116,9 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const addedTypes = DocListCast(Cast(Doc.UserDoc()["template-buttons"], Doc, null)?.data); const layout = Doc.Layout(firstDoc); const templateMenu: Array<JSX.Element> = []; - this.props.templates.forEach((checked, template) => - templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); - templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); + //this.props.templates.forEach((checked, template) => + // templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />)); + //templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />); templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />); templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); addedTypes.concat(noteTypes).map(template => template.treeViewChecked = this.templateIsUsed(firstDoc, template)); @@ -133,6 +133,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} rootSelected={returnFalse} onCheckedClick={this.scriptField} onChildClick={this.scriptField} diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx index 1b81c544a..e84022366 100644 --- a/src/client/views/animationtimeline/Keyframe.tsx +++ b/src/client/views/animationtimeline/Keyframe.tsx @@ -331,7 +331,7 @@ export class Keyframe extends React.Component<IProps> { }), TimelineMenu.Instance.addItem("button", "Show Data", action(() => { const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 }); - CollectionDockingView.AddRightSplit(kvp, emptyPath); + CollectionDockingView.AddSplit(kvp, "right"); })), TimelineMenu.Instance.addItem("button", "Delete", action(() => { (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1); diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index df828897e..1131ec874 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -83,14 +83,11 @@ export class Timeline extends React.Component<FieldViewProps> { } /////////lifecycle functions//////////// - componentWillMount() { + componentDidMount() { const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset - } - - componentDidMount() { runInAction(() => { if (!this.props.Document.AnimationLength) { //if animation length did not exist this.props.Document.AnimationLength = this._time; //set it to default time diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 6ebd5103b..96f5afcd9 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,24 +1,5 @@ @import "../../views/globalCssVariables.scss"; -.miniMap { - position: absolute; - overflow: hidden; - right: 10; - bottom: 10; - border: solid 1px; - box-shadow: black 0.4vw 0.4vw 0.8vw; - - .miniOverlay { - width: 100%; - height: 100%; - position: absolute; - - .miniThumb { - background: #25252525; - position: absolute; - } - } -} .lm_title { margin-top: 3px; @@ -69,19 +50,7 @@ } .lm_popout { - display: none; -} - -.messageCounter { - width: 18px; - height: 20px; - text-align: center; - border-radius: 20px; - margin-left: 5px; - transform: translate(0px, -8px); - display: inline-block; - background: transparent; - border: 1px #999999 solid; + display: inline; } .collectiondockingview-container { @@ -101,7 +70,7 @@ margin: auto; } - .collectionDockingView-dragAsDocument { + .collectionDockingView-drag { touch-action: none; position: absolute; padding-left: 5px; @@ -119,6 +88,10 @@ transform: scale(1.2); } + .lm_controls .lm_popout { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC) + } + .lm_maximised .lm_controls .lm_maximise { opacity: 1; transform: scale(0.8); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 43da0d3cf..c891d2035 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,46 +1,32 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import * as GoldenLayout from "../../../client/goldenLayout"; -import { DateField } from '../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; -import { FieldId } from "../../../fields/RefField"; +import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse, emptyPath, aggregateBounds } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs } from '../../documents/Documents'; -import { DocumentManager } from '../../util/DocumentManager'; -import { DragManager, dropActionType } from "../../util/DragManager"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DragManager } from "../../util/DragManager"; +import { InteractionUtils } from '../../util/InteractionUtils'; import { Scripting } from '../../util/Scripting'; -import { SelectionManager } from '../../util/SelectionManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch } from "../../util/UndoManager"; -import { MainView } from '../MainView'; -import { DocumentView } from "../nodes/DocumentView"; +import { undoBatch, UndoManager } from "../../util/UndoManager"; import "./CollectionDockingView.scss"; -import { SubCollectionViewProps } from "./CollectionSubView"; -import { DockingViewButtonSelector } from './ParentDocumentSelector'; -import React = require("react"); +import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView"; import { CollectionViewType } from './CollectionView'; -import { SnappingManager } from '../../util/SnappingManager'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { listSpec } from '../../../fields/Schema'; -import { clamp } from 'lodash'; -import { PresBox } from '../nodes/PresBox'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { InteractionUtils } from '../../util/InteractionUtils'; -import { InkTool } from '../../../fields/InkField'; +import { TabDocView } from './TabDocView'; +import React = require("react"); const _global = (window /* browser */ || global /* node */) as any; @observer -export class CollectionDockingView extends React.Component<SubCollectionViewProps> { - @observable public static Instances: CollectionDockingView[] = []; - @computed public static get Instance() { return CollectionDockingView.Instances[0]; } - public static makeDocumentConfig(document: Doc, width?: number, libraryPath?: Doc[]) { +export class CollectionDockingView extends CollectionSubView(doc => doc) { + @observable public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) { return { type: 'react-component', component: 'DocumentFrameRenderer', @@ -48,341 +34,212 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp width: width, props: { documentId: document[Id], - libraryPath: libraryPath?.map(d => d[Id]) + panelName // name of tab that can be used to close or replace its contents } }; } - @computed public get initialized() { - return this._goldenLayout !== null; - } - - @observable private _goldenLayout: any = null; + private _reactionDisposer?: IReactionDisposer; private _containerRef = React.createRef<HTMLDivElement>(); - private _flush: boolean = false; + private _flush: UndoManager.Batch | undefined; private _ignoreStateChange = ""; - private _isPointerDown = false; - private _maximizedSrc: Opt<DocumentView>; + public tabMap: Set<any> = new Set(); + public get initialized() { return this._goldenLayout !== null; } + public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } + @observable private _goldenLayout: any = null; constructor(props: SubCollectionViewProps) { super(props); - runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this)); + runInAction(() => CollectionDockingView.Instance = this); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; DragManager.StartWindowDrag = this.StartOtherDrag; } + public StartOtherDrag = (e: any, dragDocs: Doc[]) => { - let config: any; - if (dragDocs.length === 1) { - config = CollectionDockingView.makeDocumentConfig(dragDocs[0]); - } else { - config = { - type: 'row', - content: dragDocs.map((doc, i) => { - CollectionDockingView.makeDocumentConfig(doc); - }) - }; - } - const div = document.createElement("div"); - const dragSource = this._goldenLayout.createDragSource(div, config); - dragSource._dragListener.on("dragStop", () => { - dragSource.destroy(); - }); + !this._flush && (this._flush = UndoManager.StartBatch("golden layout drag")); + const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : + { type: 'row', content: dragDocs.map((doc, i) => CollectionDockingView.makeDocumentConfig(doc)) }; + const dragSource = this._goldenLayout.createDragSource(document.createElement("div"), config); + //dragSource._dragListener.on("dragStop", dragSource.destroy); dragSource._dragListener.onMouseDown(e); } @undoBatch - @action - public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) { - if (docView.props.Document._viewType === CollectionViewType.Docking && docView.props.Document.layoutKey === "layout") { - return MainView.Instance.openWorkspace(docView.props.Document); - } - const document = Doc.MakeAlias(docView.props.Document); - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - this._goldenLayout.root.contentItems[0].addChild(docconfig); - docconfig.callDownwards('_$init'); - this._goldenLayout._$maximiseItem(docconfig); - this._maximizedSrc = docView; - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + public CloseFullScreen = () => { + this._goldenLayout._maximisedItem?.toggleMaximise(); this.stateChanged(); - SelectionManager.DeselectAll(); } - public CloseFullScreen = () => { - const target = this._goldenLayout._maximisedItem; - if (target !== null && this._maximizedSrc) { - this._goldenLayout._maximisedItem.remove(); - SelectionManager.SelectDoc(this._maximizedSrc, false); - this._maximizedSrc = undefined; - this.stateChanged(); + @undoBatch + public static CloseSplit(document: Opt<Doc>, panelName?: string): boolean { + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => panelName ? tab.contentItem.config.props.panelName === panelName : tab.DashDoc === document); + if (tab) { + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + if (j !== -1) { + tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); + } } - } - public HasFullScreen = () => { - return this._goldenLayout._maximisedItem !== null; + return false; } @undoBatch - @action - public static CloseRightSplit(document: Opt<Doc>): boolean { + public static OpenFullScreen(doc: Doc, libraryPath?: Doc[]) { const instance = CollectionDockingView.Instance; - const tryClose = (childItem: any) => { - if (childItem.config?.component === "DocumentFrameRenderer") { - const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId); - if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) { - childItem.remove(); - instance.layoutChanged(document); - return true; - } - } - return false; + if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { + return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + } + const newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))] }; - const retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false : - Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose)); - - retVal && instance.stateChanged(); - return retVal; + const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + instance._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + instance._goldenLayout._$maximiseItem(docconfig); + instance._goldenLayout.emit('stateChanged'); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + instance.stateChanged(); + return true; } - @action - layoutChanged(removed?: Doc) { - this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('stateChanged'); - this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); - if (removed) CollectionDockingView.Instance._removedDocs.push(removed); - this.stateChanged(); - } @undoBatch - @action - public static ReplaceRightSplit(document: Doc, libraryPath?: Doc[], addToSplit?: boolean): boolean { - if (!CollectionDockingView.Instance) return false; + public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean { const instance = CollectionDockingView.Instance; - let retVal = false; - if (instance._goldenLayout.root.contentItems[0].isRow) { - retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { - if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) { - const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - child.addChild(newItemStackConfig, undefined); - !addToSplit && child.contentItems[0].remove(); - instance.layoutChanged(document); - return true; - } - return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { - if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) { - const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - child.addChild(newItemStackConfig, undefined); - !addToSplit && child.contentItems[j].remove(); - instance.layoutChanged(document); - return true; - } - return false; - }); - }); + if (!instance) return false; + const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName); + if (!panelName && stack) { + const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); + const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + stack.contentItems[activeContentItemIndex].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - if (retVal) { - instance.stateChanged(); + const tab = Array.from(CollectionDockingView.Instance.tabMap.keys()).find((tab) => tab.contentItem.config.props.panelName === panelName); + if (tab) { + tab.header.parent.addChild(newConfig, undefined); + const j = tab.header.parent.contentItems.indexOf(tab.contentItem); + !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove(); + return CollectionDockingView.Instance.layoutChanged(); } - return retVal; + return CollectionDockingView.AddSplit(document, panelName, stack, panelName); } - // - // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split - // - @undoBatch - @action - public static AddRightSplit(document: Doc, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; - const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; - - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else { - const collayout = instance._goldenLayout.root.contentItems[0]; - const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); - newRow.addChild(newContentItem, undefined, true); - newRow.addChild(collayout, 0, true); - - collayout.config.width = 50; - newContentItem.config.width = 50; - } - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; + @undoBatch + public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) { + return Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex((tab) => tab.DashDoc === doc) !== -1 ? + CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName); } - // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @undoBatch - @action - public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) { - if (!CollectionDockingView.Instance) return false; + public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) { + if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document); const instance = CollectionDockingView.Instance; - const newItemStackConfig = { - type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)] - }; + if (!instance) return false; + const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName); - const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); - - if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns - instance._goldenLayout.root.addChild(newContentItem); - } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row - if (pullSide === "left") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "right") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "top" || pullSide === "bottom") { - // if not going in a row layout, must add already existing content into column - const rowlayout = instance._goldenLayout.root.contentItems[0]; - const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); - rowlayout.parent.replaceChild(rowlayout, newColumn); - if (pullSide === "top") { - newColumn.addChild(rowlayout, undefined, true); - newColumn.addChild(newContentItem, 0, true); - } else if (pullSide === "bottom") { - newColumn.addChild(newContentItem, undefined, true); - newColumn.addChild(rowlayout, 0, true); - } + if (!pullSide && stack) { + stack.addChild(docContentConfig, undefined); + } else { + const newItemStackConfig = { type: 'stack', content: [docContentConfig] }; + const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); + if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns + instance._goldenLayout.root.addChild(newContentItem); + } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row + switch (pullSide) { + default: + case "right": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "top": + case "bottom": + // if not going in a row layout, must add already existing content into column + const rowlayout = instance._goldenLayout.root.contentItems[0]; + const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout); + rowlayout.parent.replaceChild(rowlayout, newColumn); + if (pullSide === "top") { + newColumn.addChild(rowlayout, undefined, true); + newColumn.addChild(newContentItem, 0, true); + } else if (pullSide === "bottom") { + newColumn.addChild(newContentItem, undefined, true); + newColumn.addChild(rowlayout, 0, true); + } - rowlayout.config.height = 50; - newContentItem.config.height = 50; - } - } else if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column - if (pullSide === "top") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); - } else if (pullSide === "bottom") { - instance._goldenLayout.root.contentItems[0].addChild(newContentItem); - } else if (pullSide === "left" || pullSide === "right") { - // if not going in a row layout, must add already existing content into column - const collayout = instance._goldenLayout.root.contentItems[0]; - const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); - collayout.parent.replaceChild(collayout, newRow); - - if (pullSide === "left") { - newRow.addChild(collayout, undefined, true); - newRow.addChild(newContentItem, 0, true); - } else if (pullSide === "right") { - newRow.addChild(newContentItem, undefined, true); - newRow.addChild(collayout, 0, true); + rowlayout.config.height = 50; + newContentItem.config.height = 50; } + } else {// if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column + switch (pullSide) { + case "top": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break; + case "bottom": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break; + case "left": + case "right": + default: + // if not going in a row layout, must add already existing content into column + const collayout = instance._goldenLayout.root.contentItems[0]; + const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout); + collayout.parent.replaceChild(collayout, newRow); + + if (pullSide === "left") { + newRow.addChild(collayout, undefined, true); + newRow.addChild(newContentItem, 0, true); + } else { + newRow.addChild(newContentItem, undefined, true); + newRow.addChild(collayout, 0, true); + } - collayout.config.width = 50; - newContentItem.config.width = 50; - } - } - - newContentItem.callDownwards('_$init'); - instance.layoutChanged(); - return true; - } - - - // - // Creates a vertical split on the right side of the docking view, and then adds the Document to that split - // - @undoBatch - @action - public static UseRightSplit(document: Doc, libraryPath?: Doc[], shiftKey?: boolean) { - document.isDisplayPanel = true; - if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, libraryPath, shiftKey)) { - CollectionDockingView.AddRightSplit(document, libraryPath); - } - } - - @undoBatch - @action - public AddTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; + collayout.config.width = 50; + newContentItem.config.width = 50; } } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); + instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig()); + newContentItem.callDownwards('_$init'); } - this.layoutChanged(); - return true; + + return instance.layoutChanged(); } @undoBatch @action - public ReplaceTab = (stack: any, document: Doc, libraryPath?: Doc[]) => { - Doc.GetProto(document).lastOpened = new DateField; - const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath); - if (stack === undefined) { - let stack: any = this._goldenLayout.root; - while (!stack.isStack) { - if (stack.contentItems.length) { - stack = stack.contentItems[0]; - } else { - stack.addChild({ type: 'stack', content: [docContentConfig] }); - stack = undefined; - break; - } - } - if (stack) { - stack.addChild(docContentConfig); - } - } else { - stack.addChild(docContentConfig, undefined); - } - this.layoutChanged(); + layoutChanged() { + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); + this.stateChanged(); return true; } - setupGoldenLayout() { + async setupGoldenLayout() { const config = StrCast(this.props.Document.dockingConfig); if (config) { - if (!this._goldenLayout) { - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); - } - else { + const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) ?? []; + await Promise.all(docids.map(id => DocServer.GetRefField(id))); + + if (this._goldenLayout) { if (config === JSON.stringify(this._goldenLayout.toConfig())) { return; + } else { + try { + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } } - try { - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); - this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - this._goldenLayout.unbind('stackCreated', this.stackCreated); - } catch (e) { } - this._goldenLayout.destroy(); - runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); } - this._goldenLayout.on('itemDropped', this.itemDropped); + this.tabMap.clear(); + this._goldenLayout?.destroy(); + runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config))); this._goldenLayout.on('tabCreated', this.tabCreated); this._goldenLayout.on('tabDestroyed', this.tabDestroyed); this._goldenLayout.on('stackCreated', this.stackCreated); - this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.registerComponent('DocumentFrameRenderer', TabDocView); this._goldenLayout.container = this._containerRef.current; if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { try { @@ -394,582 +251,152 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } - reactionDisposer?: Lambda; + componentDidMount: () => void = () => { if (this._containerRef.current) { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this.onResize(null as any); - } - })); - observer.observe(this._containerRef.current); - this.reactionDisposer = reaction( - () => this.props.Document.dockingConfig, - () => { - if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { - // Because this is in a set timeout, if this component unmounts right after mounting, - // we will leak a GoldenLayout, because we try to destroy it before we ever create it - setTimeout(() => this.setupGoldenLayout(), 1); - DocListCast((Doc.UserDoc().myWorkspaces as Doc).data).map(d => d.workspaceBrush = false); - this.props.Document.workspaceBrush = true; + new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); + this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig), + config => { + if (!this._goldenLayout || this._ignoreStateChange !== config) { // bcz: TODO! really need to diff config with ignoreStateChange and modify the current goldenLayout instead of building a new one. + this.setupGoldenLayout(); } this._ignoreStateChange = ""; - }, { fireImmediately: true }); - - window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + }); + setTimeout(() => this.setupGoldenLayout(), 0); + //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } + componentWillUnmount: () => void = () => { try { - this.props.Document.workspaceBrush = false; - this._goldenLayout.unbind('itemDropped', this.itemDropped); - this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('stackCreated', this.stackCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - } catch (e) { - - } - this._goldenLayout && this._goldenLayout.destroy(); - runInAction(() => { - CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1); - this._goldenLayout = null; - }); + } catch (e) { } + this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); - this.reactionDisposer && this.reactionDisposer(); + this._reactionDisposer?.(); } + @action onResize = (event: any) => { const cur = this._containerRef.current; - // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - this._goldenLayout?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); } @action - onPointerUp = (e: React.PointerEvent): void => { + onPointerUp = (e: MouseEvent): void => { + window.removeEventListener("pointerup", this.onPointerUp); if (this._flush) { - this._flush = false; setTimeout(() => { CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); this.stateChanged(); + this._flush!.end(); + this._flush = undefined; }, 10); } } + @action onPointerDown = (e: React.PointerEvent): void => { - this._isPointerDown = true; - const onPointerUp = action(() => { - window.removeEventListener("pointerup", onPointerUp); - this._isPointerDown = false; - }); - window.addEventListener("pointerup", onPointerUp); - const className = (e.target as any).className; - if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { - this._flush = true; + window.addEventListener("mouseup", this.onPointerUp); + if (!(e.target as HTMLElement).closest("*.lm_content") && ((e.target as HTMLElement).closest("*.lm_tab") || (e.target as HTMLElement).closest("*.lm_stack"))) { + this._flush = UndoManager.StartBatch("golden layout edit"); } - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { - return; - } else { + if (!e.nativeEvent.cancelBubble && !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && + Doc.GetSelectedTool() !== InkTool.Highlighter && Doc.GetSelectedTool() !== InkTool.Pen) { e.stopPropagation(); } } - updateDataField = async (json: string) => { + public static Copy(doc: Doc) { + let json = StrCast(doc.dockingConfig); const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); - const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")) || []; + const docs = docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); + const newtabs = docs.map(doc => { + const copy = Doc.MakeAlias(doc); + json = json.replace(doc[Id], copy[Id]); + return copy; + }); + const copy = Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title }); + const docsublists = DocListCast(doc.data); + const copysublists = DocListCast(copy.data); + const docother = Cast(docsublists[1], Doc, null); + const copyother = Cast(copysublists[1], Doc, null); + const newother = DocListCast(docother.data).map(doc => Doc.MakeAlias(doc)); + Doc.GetProto(copyother).data = new List<Doc>(newother); - if (docids) { - const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); - docs.map(doc => Doc.AddDocToList(Doc.GetProto(this.props.Document), this.props.fieldKey, doc)); - // Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); - } + return copy; } - @undoBatch + @action stateChanged = () => { const json = JSON.stringify(this._goldenLayout.toConfig()); - this.props.Document.dockingConfig = json; - this.updateDataField(json); - } - - itemDropped = () => { - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - this.stateChanged(); - } - - htmlToElement(html: string) { - const template = document.createElement('template'); - html = html.trim(); // Never return a text node of whitespace as the result - template.innerHTML = html; - return template.content.firstChild; - } - - tabCreated = async (tab: any) => { - tab.titleElement[0].Tab = tab; - if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - if (tab.contentItem.config.fixed) { - tab.contentItem.parent.config.fixed = true; - } + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace("\"documentId\":\"", "").replace("\"", "")); + const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; - if (doc instanceof Doc) { - tab.titleElement[0].onclick = (e: any) => { - if (Date.now() - tab.titleElement[0].lastClick < 1000) tab.titleElement[0].select(); - tab.titleElement[0].lastClick = Date.now(); - tab.titleElement[0].focus(); - }; - tab.titleElement[0].onchange = (e: any) => { - tab.titleElement[0].size = e.currentTarget.value.length + 1; - Doc.GetProto(doc).title = e.currentTarget.value, true; - }; - tab.titleElement[0].size = StrCast(doc.title).length + 1; - tab.titleElement[0].value = doc.title; - tab.titleElement[0].style["max-width"] = "100px"; - const gearSpan = document.createElement("span"); - gearSpan.className = "collectionDockingView-gear"; - gearSpan.style.position = "relative"; - gearSpan.style.paddingLeft = "0px"; - gearSpan.style.paddingRight = "12px"; - const stack = tab.contentItem.parent; - tab.element[0].onpointerdown = (e: any) => { - const view = DocumentManager.Instance.getDocumentView(doc); - view && SelectionManager.SelectDoc(view, false); - }; - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: any) => { - if (!this._isPointerDown || !SnappingManager.GetIsDragging()) return; - const activeContentItem = tab.header.parent.getActiveContentItem(); - if (tab.contentItem !== activeContentItem) { - tab.header.parent.setActiveContentItem(tab.contentItem); - } - tab.setActive(true); - }; - const onDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e) => { - if (!(e as any).defaultPrevented) { - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction as dropActionType; - DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - return true; - } - return false; - }, returnFalse, emptyFunction); - }; - - tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)), - (views) => { - if (views.length) { - ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} > - <DockingViewButtonSelector views={() => views} Stack={stack} /> - </span>, - gearSpan); - tab.buttonDisposer?.(); - } - }, { fireImmediately: true }); - - tab.reactComponents = [gearSpan]; - tab.element.append(gearSpan); - tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { - tab.titleElement[0].textContent = title, { fireImmediately: true }; - tab.titleElement[0].style.padding = degree ? 0 : 2; - tab.titleElement[0].style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; - }); - //TODO why can't this just be doc instead of the id? - tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; - } - } - tab.closeElement.off('click') //unbind the current click handler - .click(async function () { - tab.reactionDisposer?.(); - tab.buttonDisposer?.(); - const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId); - if (doc instanceof Doc) { - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - - const recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc); - if (recent) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - SelectionManager.DeselectAll(); - } - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - tab.contentItem.remove(); - CollectionDockingView.Instance._ignoreStateChange = JSON.stringify(CollectionDockingView.Instance._goldenLayout.toConfig()); - }); + this.props.Document.dockingConfig = json; + const sublists = DocListCast(this.props.Document[this.props.fieldKey]); + const tabs = Cast(sublists[0], Doc, null); + const other = Cast(sublists[1], Doc, null); + const tabdocs = DocListCast(tabs.data); + const otherdocs = DocListCast(other.data); + Doc.GetProto(tabs).data = new List<Doc>(docs); + const otherSet = new Set<Doc>(); + otherdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + tabdocs.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); + Doc.GetProto(other).data = new List<Doc>(Array.from(otherSet.values())); } tabDestroyed = (tab: any) => { - if (tab.reactComponents) { - for (const ele of tab.reactComponents) { - ReactDOM.unmountComponentAtNode(ele); - } - } + this.tabMap.delete(tab); + Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + } + tabCreated = (tab: any) => { + tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous abs (ie, when dragging a tab around a new tab is created for the old content) } - _removedDocs: Doc[] = []; stackCreated = (stack: any) => { - //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.element.on('mousedown', (e: any) => { - if (e.target === stack.header.element[0] && e.button === 1) { - this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); + stack.header?.element.on('mousedown', (e: any) => { + if (e.target === stack.header?.element[0] && e.button === 2) { + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); } }); - // starter code for bezel to add new pane - // stack.element.on("touchstart", (e: TouchEvent) => { - // if (e.targetTouches.length === 2) { - // let pt1 = e.targetTouches.item(0); - // let pt2 = e.targetTouches.item(1); - // let threshold = 40 * window.devicePixelRatio; - // if (pt1 && pt2 && InteractionUtils.TwoPointEuclidist(pt1, pt2) < threshold) { - // let edgeThreshold = 30 * window.devicePixelRatio; - // let center = InteractionUtils.CenterPoint([pt1, pt2]); - // let stackRect: DOMRect = stack.element.getBoundingClientRect(); - // let nearLeft = center.X - stackRect.x < edgeThreshold; - // let nearTop = center.Y - stackRect.y < edgeThreshold; - // let nearRight = stackRect.right - center.X < edgeThreshold; - // let nearBottom = stackRect.bottom - center.Y < edgeThreshold; - // let ns = [nearLeft, nearTop, nearRight, nearBottom].filter(n => n); - // if (ns.length === 1) { - - // } - // } - // } - // }); - stack.header.controlsContainer.find('.lm_close') //get the close icon + stack.header?.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler - .click(action(async function () { + .click(action(() => { //if (confirm('really close this?')) { - stack.remove(); - stack.contentItems.forEach(async (contentItem: any) => { - const doc = await DocServer.GetRefField(contentItem.config.props.documentId); - if (doc instanceof Doc) { - let recent: Doc | undefined; - if (recent = await Cast(Doc.UserDoc().myRecentlyClosed, Doc)) { - Doc.AddDocToList(recent, "data", doc, undefined, true, true); - } - const theDoc = doc; - CollectionDockingView.Instance._removedDocs.push(theDoc); - } - }); - //} + stack.contentItems.forEach((contentItem: any) => Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", contentItem.tab.DashDoc, undefined, true, true)); })); - stack.header.controlsContainer.find('.lm_popout') //get the close icon + stack.header?.controlsContainer.find('.lm_popout') //get the close icon .off('click') //unbind the current click handler - .click(action(function () { - stack.config.fixed = !stack.config.fixed; - // const url = Utils.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId); - // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); + .click(action(() => { + // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size + const emptyPane = CurrentUserUtils.EmptyPane; + emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + }), "", stack); })); } render() { - if (this.props.renderDepth > 0) { - return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; - } - return <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />; - } - -} - -interface DockedFrameProps { - documentId: FieldId; - glContainer: any; - libraryPath: (FieldId[]); - //collectionDockingView: CollectionDockingView -} -@observer -export class DockedFrameRenderer extends React.Component<DockedFrameProps> { - _mainCont: HTMLDivElement | null = null; - @observable private _libraryPath: Doc[] = []; - @observable private _panelWidth = 0; - @observable private _panelHeight = 0; - @observable private _document: Opt<Doc>; - @observable private _isActive: boolean = false; - _tabReaction: IReactionDisposer | undefined; - - get _stack(): any { - return (this.props as any).glContainer.parent.parent; - } - get _tab(): any { - const tab = (this.props as any).glContainer.tab?.element[0] as HTMLElement; - return tab?.getElementsByClassName("lm_title")?.[0]; - } - constructor(props: any) { - super(props); - DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); - this.props.libraryPath && this.setupLibraryPath(); - } - - async setupLibraryPath() { - Promise.all(this.props.libraryPath.map(async docid => { - const d = await DocServer.GetRefField(docid); - return d instanceof Doc ? d : undefined; - })).then(action((list: (Doc | undefined)[]) => this._libraryPath = list.filter(d => d).map(d => d as Doc))); - } - - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static PinDoc(doc: Doc, unpin = false) { - if (unpin) DockedFrameRenderer.UnpinDoc(doc); - else { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.presZoomButton = true; - pinDoc.context = curPres; - Doc.AddDocToList(curPres, "data", pinDoc); - if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - if (!DocumentManager.Instance.getDocumentView(curPres)) { - CollectionDockingView.AddRightSplit(curPres); - } - DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); - } - } - } - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static UnpinDoc(doc: Doc) { - //add this new doc to props.Document - const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; - if (curPres) { - const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); - ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); - } - } - - componentDidMount() { - const observer = new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this._panelWidth = entry.contentRect.width; - this._panelHeight = entry.contentRect.height; - } - })); - observer.observe(this.props.glContainer._element[0]); - this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.on("tab", this.onActiveContentItemChanged); - this.onActiveContentItemChanged(); - this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: StrCast(this._document?._backgroundColor, this._document && CollectionDockingView.Instance?.props.backgroundColor?.(this._document, 0) || "white") }), - (data) => { - const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); - this._tab && (this._tab.style.backgroundColor = selected ? data.color : ""); - } - ); - } - - componentWillUnmount() { - this._tabReaction?.(); - this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.off("tab", this.onActiveContentItemChanged); - } - - @action.bound - private onActiveContentItemChanged() { - if (this.props.glContainer.tab) { - this._isActive = this.props.glContainer.tab.isActive; - setTimeout(() => { - const dv = this._document && DocumentManager.Instance.getFirstDocumentView(this._document); - dv && SelectionManager.SelectDoc(dv, false); - }); - !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. - } - } - - get layoutDoc() { return this._document && Doc.Layout(this._document); } - nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; - panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : - (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) - panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; - - nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0; - nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0; - - contentScaling = () => { - const nativeH = this.nativeHeight(); - const nativeW = this.nativeWidth(); - let scaling = 1; - if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { - scaling = 1; - } else if (NumCast(this.layoutDoc!._nativeWidth) && ((this.layoutDoc?._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth))) { - scaling = this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - } else if (nativeW && nativeH) { - // if (this.layoutDoc!.type === DocumentType.PDF || this.layoutDoc!.type === DocumentType.WEB) { - // if ((this.layoutDoc?._fitWidth) || - // this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { - // return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth); - // } else { - // return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight); - // } - // } - const wscale = this.panelWidth() / nativeW; - scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } else scaling = 1; - return scaling; - } - - ScreenToLocalTransform = () => { - if (this._mainCont && this._mainCont.children) { - const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement); - const scale = Utils.GetScreenTransform(this._mainCont).scale; - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); - } - return Transform.Identity(); - } - get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } - get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}%` : undefined; } - - addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { - SelectionManager.DeselectAll(); - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") { - return MainView.Instance.openWorkspace(doc); - } else if (location === "onRight") { - return CollectionDockingView.AddRightSplit(doc, libraryPath); - } else if (location === "close") { - return CollectionDockingView.CloseRightSplit(doc); - } else if (location === "replace") { - CollectionDockingView.UseRightSplit(doc); - return true; - } else {// if (location === "inPlace") { - return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath); - } - } - - @computed get renderContentBounds() { - const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; - const xbounds = bounds[2] - bounds[0]; - const ybounds = bounds[3] - bounds[1]; - const dim = Math.max(xbounds, ybounds); - return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; - } - @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; } - @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; } - @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } - childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim); - this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim); - return false; - }), emptyFunction, emptyFunction); - } - getCurrentFrame = (): number => { - const presTargetDoc = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null); - const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); - return currentFrame; - } - - renderMiniMap() { - return <div className="miniMap" style={{ - width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, - StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!, 0))), - }}> - <CollectionFreeFormView - Document={this._document!} - LibraryPath={emptyPath} - CollectionView={undefined} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this. - noOverlay={true} // don't render overlay Docs since they won't scale - active={returnTrue} - select={emptyFunction} - dropAction={undefined} - isSelected={returnFalse} - dontRegisterView={true} - annotationsKey={""} - fieldKey={Doc.LayoutFieldKey(this._document!)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - ContentScaling={returnOne} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - NativeHeight={returnZero} - NativeWidth={returnZero} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - fitToBox={true} - /> - <div className="miniOverlay" onPointerDown={this.miniDown} > - <div className="miniThumb" style={{ - width: `${this.miniWidth}%`, - height: `${this.miniHeight}%`, - left: `${this.miniLeft}%`, - top: `${this.miniTop}%`, - }} - /> - </div> + return <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef}> + {this.props.renderDepth > 0 ? "Nested dashboards can't be rendered" : (null)} </div>; } - @computed get docView() { - TraceMobx(); - if (!this._document) return (null); - const document = this._document; - const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined; - return <> - <DocumentView key={document[Id]} - LibraryPath={this._libraryPath} - Document={document} - DataDoc={resolvedDataDoc} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={undefined} - removeDocument={undefined} - ContentScaling={this.contentScaling} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - NativeHeight={this.nativeHeight} - NativeWidth={this.nativeWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - renderDepth={0} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - focus={emptyFunction} - backgroundColor={CollectionDockingView.Instance.props.backgroundColor} - addDocTab={this.addDocTab} - pinToPres={DockedFrameRenderer.PinDoc} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - {document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} - </>; - } - - render() { - return (!this._isActive || !this.layoutDoc) ? (null) : - (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref} - style={{ - transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, - height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%", - width: this.widthpercent - }}> - {this.docView} - </div >); - } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddSplit(doc, "right"); }, "opens up the inputted document on the right side of the screen", "(doc: any)"); -Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/CollectionDockingViewMenu.scss index bc9cf4848..4157f0f7e 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/CollectionDockingViewMenu.scss @@ -1,15 +1,14 @@ -.parentDocumentSelector-linkFlyout { + +.dockingViewButtonSelector { div { overflow: visible !important; } - .metadataEntry-outerDiv { - overflow: hidden !important; - pointer-events: all; - } + display: inline-block; + width:100%; + height:100%; } - -.parentDocumentSelector-flyout { +.dockingViewButtonSelector-flyout { position: relative; z-index: 9999; background-color: #eeeeee; @@ -32,32 +31,4 @@ border-right: 0px; border-left: 0px; } -} - -.parentDocumentSelector-button { - pointer-events: all; - position: relative; - display: inline-block; - - svg { - // width:20px !important; - //height:20px; - } -} - -.parentDocumentSelector-metadata { - pointer-events: auto; - padding-right: 5px; - width: 25px; - display: inline-block; -} - -.buttonSelector { - div { - overflow: visible !important; - } - - display: inline-block; - width:100%; - height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionDockingViewMenu.tsx b/src/client/views/collections/CollectionDockingViewMenu.tsx new file mode 100644 index 000000000..1cab293a8 --- /dev/null +++ b/src/client/views/collections/CollectionDockingViewMenu.tsx @@ -0,0 +1,48 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import { DocumentButtonBar } from "../DocumentButtonBar"; +import { DocumentView } from "../nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +export class CollectionDockingViewMenu extends React.Component<{ views: () => DocumentView[], Stack: any }> { + customStylesheet(styles: any) { + return { + ...styles, + panel: { + ...styles.panel, + minWidth: "100px" + }, + }; + } + _ref = React.createRef<HTMLDivElement>(); + + @computed get flyout() { + return ( + <div className="dockingViewButtonSelector-flyout" title=" " ref={this._ref}> + <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> + </div> + ); + } + + @observable _tooltipOpen: boolean = false; + render() { + return <Tooltip open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> + <span className="dockingViewButtonSelector" + onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))} + onPointerDown={action(e => { + this.props.views()[0]?.select(false); + this._tooltipOpen = false; + })} > + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> + <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> + </Flyout> + </span> + </Tooltip >; + } +} diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index f5c4299a9..ca72b98a5 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -4,10 +4,12 @@ .collectionLinearView-outer { overflow: visible; height: 100%; + pointer-events: none; .collectionLinearView { display: flex; height: 100%; + align-items: center; >span { background: $dark-color; diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index e1b07077e..9eaa02bf8 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -17,6 +17,7 @@ import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; import { Tooltip } from '@material-ui/core'; +import { all } from 'bluebird'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -89,6 +90,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { } } DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } @action @@ -112,12 +114,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); const color = StrCast(this.props.Document.color, "white"); - const menuOpener = <label htmlFor={`${guid}`} style={{ - background: backgroundColor === color ? "black" : backgroundColor, - // width: "18px", height: "18px", fontSize: "12.5px", - // transition: this.props.Document.linearViewIsExpanded ? "transform 0.2s" : "transform 0.5s", - // transform: this.props.Document.linearViewIsExpanded ? "" : "rotate(45deg)" - }} + const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "default", background: backgroundColor === color ? "black" : backgroundColor, }} onPointerDown={e => e.stopPropagation()} > <p>{BoolCast(this.props.Document.linearViewIsExpanded) ? "–" : "+"}</p> </label>; @@ -139,6 +136,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { const scalable = pair.layout.onClick || pair.layout.onDragStart; return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} style={{ + pointerEvents: "all", width: scalable ? (nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize) : undefined, height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize, }} > @@ -166,6 +164,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; @@ -192,9 +191,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { </span> </Tooltip> - {/* <FontAwesomeIcon icon="times-circle" size="lg" style={{ color: "red" }} - onClick={this.exitLongLinks} /> */} - </span> : null} </div> </div>; diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index cfec3a6bc..1af1a05aa 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -69,7 +69,7 @@ export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMap if (!this._initialLookupPending.get(id)) { this._initialLookupPending.set(id, true); setTimeout(() => { - titleLoc && Doc.SetInPlace(doc, "title", titleLoc, true); + titleLoc && Doc.SetInPlace(doc, `${fieldKey}-address`, titleLoc, true); this.respondToAddressChange(doc, fieldKey, address).then(() => this._initialLookupPending.delete(id)); }); } @@ -114,12 +114,12 @@ export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMap } } - private renderMarker = (layout: Doc) => { - const location = this.getLocation(layout, Doc.LayoutFieldKey(layout)); + private renderMarker = (layout: Doc, fieldKey?: string) => { + const location = this.getLocation(layout, fieldKey || Doc.LayoutFieldKey(layout)); return !location ? (null) : <Marker key={layout[Id]} - label={StrCast(layout.title)} + label={StrCast(layout[`${this.props.fieldKey}-address`])} position={location} onClick={() => this.markerClick(layout, location)} icon={this.renderMarkerIcon(layout)} @@ -250,7 +250,7 @@ export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMap }} > {this.reactiveContents} - {mapLoc ? this.renderMarker(this.rootDoc) : undefined} + {mapLoc && StrCast(this.rootDoc[`${fieldKey}-mapCenter-address`]) ? this.renderMarker(this.rootDoc, `${fieldKey}-mapCenter`) : undefined} </GeoMap> </div> </div>; @@ -260,9 +260,10 @@ export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMap export default GoogleApiWrapper({ apiKey: process.env.GOOGLE_MAPS!, - LoadingContainer: () => ( - <div className={"loadingWrapper"}> + LoadingContainer: () => { + console.log(process.env.GOOGLE_MAPS); + return <div className={"loadingWrapper"}> <img className={"loadingGif"} src={"/assets/loading.gif"} /> - </div> - ) + </div>; + } })(CollectionMapView) as any;
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index c772dcfe7..1c96f69bf 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -140,7 +140,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr addDocument = (value: string, shiftDown?: boolean) => { this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); - const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value }); + const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 200, title: value }); newDoc[key] = this.getValue(this.props.heading); const docs = this.props.parent.childDocList; return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc); diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 8658212b7..e36e5caa7 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -229,14 +229,18 @@ .flexLabel { margin-bottom: 0; } - } - .collectionGridViewChrome-entryBox { - width: 50%; + .collectionGridViewChrome-entryBox { + width: 50%; + color: black; + } + + .collectionGridViewChrome-columnButton { + color: black; + } } } - .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; @@ -311,6 +315,59 @@ } } +.webBox-urlEditor { + position: relative; + opacity: 0.9; + z-index: 9001; + transition: top .5s; + + .urlEditor { + display: grid; + grid-template-columns: 1fr auto; + padding-bottom: 10px; + overflow: hidden; + margin-top: 5px; + height: 35px; + + .editorBase { + display: flex; + + .editor-collapse { + transition: all .5s, opacity 0.3s; + position: absolute; + width: 40px; + transform-origin: top left; + } + + .switchToText { + color: $main-accent; + } + + .switchToText:hover { + color: $dark-color; + } + } + + button:hover { + transform: scale(1); + } + } +} + +.webpage-urlInput { + padding: 12px 10px 11px 10px; + border: 0px; + color: black; + font-size: 10px; + letter-spacing: 2px; + outline-color: black; + background: rgb(238, 238, 238); + width: 100%; + min-width: 350px; + margin-right: 10px; + height: 100%; +} + .collectionFreeFormMenu-cont { display: inline-flex; position: relative; @@ -320,6 +377,7 @@ .antimodeMenu-button { text-align: center; display: block; + position: relative; } .color-previewI { @@ -327,12 +385,15 @@ height: 20%; bottom: 0; position: absolute; + margin-left: 2px; } .color-previewII { width: 80%; height: 80%; margin-left: 10%; + position: absolute; + bottom: 5; } .btn-group { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 5119ff6c9..b04c9c2eb 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -5,45 +5,48 @@ import { Tooltip } from "@material-ui/core"; import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; -import { Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Document } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; import { ObjectField } from "../../../fields/ObjectField"; -import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; +import { WebField } from "../../../fields/URLField"; import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; +import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; import { undoBatch } from "../../util/UndoManager"; -import AntimodeMenu from "../AntimodeMenu"; +import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; -import GestureOverlay from "../GestureOverlay"; +import { GestureOverlay } from "../GestureOverlay"; import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; -import RichTextMenu from "../nodes/formattedText/RichTextMenu"; +import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; +import { PresBox } from "../nodes/PresBox"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { TabDocView } from "./TabDocView"; @observer -export default class CollectionMenu extends AntimodeMenu { - static Instance: CollectionMenu; +export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { + @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined; @observable FieldKey: string; - constructor(props: Readonly<{}>) { + constructor(props: any) { super(props); this.FieldKey = ""; - CollectionMenu.Instance = this; + runInAction(() => CollectionMenu.Instance = this); this._canFade = false; // don't let the inking menu fade away - this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true); + runInAction(() => this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true)); this.jumpTo(300, 300); } @@ -130,7 +133,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp _contentCommand = { params: ["target", "source"], title: "set content", script: "getProto(self.target).data = copyField(self.source);", - immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), + immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), initialize: emptyFunction, }; _onClickCommand = { @@ -161,9 +164,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _viewCommand = { params: ["target"], title: "bookmark view", - script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale'];", - immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; }), - initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; }, + script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale']; gotoFrame(self.target, self['target-currentFrame']);", + immediate: undoBatch((source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target._viewScale = 1; this.target._currentFrame = 0; }), + initialize: (button: Doc) => { button['target-panX'] = this.target._panX; button['target-panY'] = this.target._panY; button['target-viewScale'] = this.target._viewScale; button['target-currentFrame'] = this.target._currentFrame; }, }; _clusterCommand = { params: ["target"], title: "fit content", @@ -173,18 +176,22 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp }; _fitContentCommand = { params: ["target"], title: "toggle clusters", - script: "self.target.useClusters = !self.target.useClusters;", - immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), + script: "self.target._useClusters = !self.target._useClusters;", + immediate: undoBatch((source: Doc[]) => this.target._useClusters = !this.target._useClusters), initialize: emptyFunction }; _saveFilterCommand = { params: ["target"], title: "save filter", - script: "self.target._docFilters = copyField(self['target-docFilters']);", - immediate: undoBatch((source: Doc[]) => this.target._docFilters = undefined), - initialize: (button: Doc) => { button['target-docFilters'] = this.target._docFilters instanceof ObjectField ? ObjectField.MakeCopy(this.target._docFilters as any as ObjectField) : ""; }, + script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']); + self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, + immediate: undoBatch((source: Doc[]) => { this.target._docFilters = undefined; this.target._searchFilterDocs = undefined; }), + initialize: (button: Doc) => { + button['target-docFilters'] = Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters instanceof ObjectField ? ObjectField.MakeCopy(Cast(Doc.UserDoc().mySearchPanelDoc, Doc, null)._docFilters as any as ObjectField) : undefined; + button['target-searchFilterDocs'] = CurrentUserUtils.ActiveDashboard._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(CurrentUserUtils.ActiveDashboard._searchFilterDocs as any as ObjectField) : undefined; + }, }; - @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand, this._saveFilterCommand] : [this._viewCommand, this._saveFilterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; } @computed get _stacking_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _masonry_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._contentCommand, this._templateCommand]; } @computed get _schema_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._templateCommand, this._narrativeCommand]; } @@ -366,6 +373,92 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } else return false; } + @computed + get pinButton() { + const targetDoc = this.selectedDoc; + const isPinned = targetDoc && Doc.isDocPinned(targetDoc); + return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top"> + <button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderRight: "1px solid gray" }} + onClick={e => TabDocView.PinDoc(targetDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-pin" /> + </button> + </Tooltip>; + } + + @undoBatch + onAlias = () => { + if (this.selectedDoc && this.selectedDocumentView) { + // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); + // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + // copy.y = NumCast(this.selectedDoc.y) + 30; + // this.selectedDocumentView.props.addDocument?.(copy); + const alias = Doc.MakeAlias(this.selectedDoc); + alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + alias.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(alias); + } + } + private _dragRef = React.createRef<HTMLButtonElement>(); + + @observable _aliasDown = false; + onAliasButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + } + + @undoBatch + onAliasButtonMoved = (e: PointerEvent) => { + if (this._dragRef.current && this.selectedDoc) { + const dragData = new DragManager.DocumentDragData([this.selectedDoc]); + const [left, top] = [e.clientX, e.clientY]; + dragData.dropAction = "alias"; + DragManager.StartDocumentDrag([this._dragRef.current], dragData, left, top, { + offsetX: dragData.offset[0], + offsetY: dragData.offset[1], + hideSource: false + }); + return true; + } + return false; + } + + @computed + get aliasButton() { + const targetDoc = this.selectedDoc; + return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Tap or Drag to create an alias"}</div>} placement="top"> + <button className="antimodeMenu-button" ref={this._dragRef} onPointerDown={this.onAliasButtonDown} onClick={this.onAlias}> + <FontAwesomeIcon className="documentdecorations-icon" icon="copy" size="lg" /> + </button> + </Tooltip>; + } + + @computed + get pinWithViewButton() { + const targetDoc = this.selectedDoc; + if (targetDoc) { + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + } + return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin to presentation with current view"}</div></>} placement="top"> + <button className="antidmodeMenu-button" style={{ borderRight: "1px solid gray" }} + onClick={e => { + if (targetDoc) { + TabDocView.PinDoc(targetDoc, false); + const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1]; + const x = targetDoc._panX; + const y = targetDoc._panY; + const scale = targetDoc._viewScale; + activeDoc.presPinView = true; + activeDoc.presPinViewX = x; + activeDoc.presPinViewY = y; + activeDoc.presPinViewScale = scale; + } + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon="map-marker" /> + </button> + </Tooltip>; + } + render() { return ( @@ -391,6 +484,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> </button> </Tooltip>} + {this.aliasButton} + {this.pinButton} + {this.pinWithViewButton} </div> {this.subChrome} </div> @@ -429,7 +525,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @computed get isText() { if (this.selectedDoc) { - return this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + const layoutField = Doc.LayoutField(this.selectedDoc); + return StrCast(layoutField).includes("FormattedText") || + (layoutField instanceof Doc && StrCast(layoutField.layout).includes("FormattedText")); } else return false; } @@ -437,25 +535,25 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @undoBatch @action nextKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0); - this.document.currentFrame = Math.max(0, (currentFrame || 0) + 1); - this.document.lastFrame = Math.max(NumCast(this.document.currentFrame), NumCast(this.document.lastFrame)); + this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1); + this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame)); } @undoBatch @action prevKeyframe = (): void => { - const currentFrame = Cast(this.document.currentFrame, "number", null); + const currentFrame = Cast(this.document._currentFrame, "number", null); if (currentFrame === undefined) { - this.document.currentFrame = 0; + this.document._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.document.currentFrame = Math.max(0, (currentFrame || 0) - 1); + this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1); } @undoBatch @action @@ -605,6 +703,141 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </div>; } + onUrlDrop = (e: React.DragEvent) => { + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const uri = dataTransfer.getData("text/uri-list"); + const url = uri || html || this._url; + const newurl = url.startsWith(window.location.origin) ? + url.replace(window.location.origin, this._url.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + this.submitURL(newurl); + e.stopPropagation(); + } + onUrlDragover = (e: React.DragEvent) => { + e.preventDefault(); + } + + @computed get _url() { + return this.selectedDoc?.data instanceof WebField ? Cast(this.selectedDoc.data, WebField, null)?.url.toString() : Field.toString(this.selectedDoc?.data as Field); + } + + set _url(value) { + if (this.selectedDoc) { + Doc.GetProto(this.selectedDoc).data = new WebField(value); + Doc.SetInPlace(this.selectedDoc, "title", value, true); + const annots = Doc.GetProto(this.selectedDoc)["data-annotations-" + this.urlHash(value)]; + Doc.GetProto(this.selectedDoc)["data-annotations"] = annots instanceof ObjectField ? ObjectField.MakeCopy(annots) : new List<Doc>([]); + } + } + + @action + submitURL = (url: string) => { + if (!url.startsWith("http")) url = "http://" + url; + try { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const URLy = new URL(url); + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + const annos = DocListCast(selectedDoc["data-annotations"]); + if (Field.toString(selectedDoc.data as Field) === Field.toString(new WebField(URLy))) { + Doc.GetProto(selectedDoc).data = undefined; + setTimeout(action(() => Doc.GetProto(selectedDoc).data = new WebField(URLy)), 0); + } else { + if (url) { + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(annos); + if (history === undefined) { + selectedDoc["data-history"] = new List<string>([this._url]); + } else { + history.push(this._url); + } + future && (future.length = 0); + } + this._url = url; + } + } + } catch (e) { + console.log("WebBox URL error:" + url); + } + } + + urlHash(s: string) { + return s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0); + } + + onValueKeyDown = async (e: React.KeyboardEvent) => { + e.key === "Enter" && this.submitURL(this._keyInput.current!.value); + e.stopPropagation(); + } + + @action + forward = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (future?.length) { + history?.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = future.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + @action + back = () => { + const selectedDoc = this.selectedDoc; + if (selectedDoc) { + const future = Cast(selectedDoc["data-future"], listSpec("string"), null); + const history = Cast(selectedDoc["data-history"], listSpec("string"), null); + if (history?.length) { + if (future === undefined) selectedDoc["data-future"] = new List<string>([this._url]); + else future.push(this._url); + Doc.GetProto(selectedDoc)["data-annotations-" + this.urlHash(this._url)] = new List<Doc>(DocListCast(selectedDoc["data-annotations"])); + const newurl = history.pop()!; + Doc.GetProto(selectedDoc).data = new WebField(new URL(this._url = newurl)); + Doc.GetProto(selectedDoc)["data-annotations"] = new List<Doc>(DocListCast(selectedDoc["data-annotations-" + this.urlHash(newurl)])); + } + } + } + + private _keyInput = React.createRef<HTMLInputElement>(); + + @computed get urlEditor() { + return ( + <div className="webBox-buttons" + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} style={{ display: "flex" }}> + <input className="webpage-urlInput" key={this._url} + placeholder="ENTER URL" + defaultValue={this._url} + onDrop={this.onUrlDrop} + onDragOver={this.onUrlDragover} + onKeyDown={this.onValueKeyDown} + onClick={(e) => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + ref={this._keyInput} + /> + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> + <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> + GO + </button> + <button className="submitUrl" onClick={this.back}> + <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> + </button> + <button className="submitUrl" onClick={this.forward}> + <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> + </button> + </div> + </div> + ); + } + + @observable viewType = this.selectedDoc?._viewType; render() { @@ -625,7 +858,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu {!this.isText && !this.props.isDoc ? <Tooltip key="num" title={<div className="dash-tooltip">Toggle View All</div>} placement="bottom"> <div className="numKeyframe" style={{ color: this.document.editing ? "white" : "black", backgroundColor: this.document.editing ? "#5B9FDD" : "#AEDDF8" }} onClick={action(() => this.document.editing = !this.document.editing)} > - {NumCast(this.document.currentFrame)} + {NumCast(this.document._currentFrame)} </div> </Tooltip> : null} {!this.isText && !this.props.isDoc ? <Tooltip key="fwd" title={<div className="dash-tooltip">Forward Frame</div>} placement="bottom"> @@ -635,16 +868,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </Tooltip> : null} {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : - <Tooltip key="hypothesis" title={<div className="dash-tooltip">Use Hypothesis</div>} placement="bottom"> - <button className={"antimodeMenu-button"} key="hypothesis" - style={{ - backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, - borderRight: "1px solid gray" - }} - onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> - <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> - </button> - </Tooltip> + this.urlEditor } {!this.isText ? <> @@ -655,7 +879,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </> : (null) } - {this.isText ? <RichTextMenu key="rich" /> : null} + {this.isText ? <RichTextMenu /> : null} </div>; } } @@ -677,15 +901,14 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu if (docs instanceof Doc) { const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || - (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key[0] !== "_")); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || - key.indexOf("author") >= 0 || key.indexOf("creationDate") >= 0 || - key.indexOf("lastModified") >= 0 || (key[0].toUpperCase() === key[0] && - key.substring(0, 3) !== "ACL" && key !== "UseCors" && key[0] !== "_")); + const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 || + key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 || + (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "ACL" && key[0] !== "_")); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } } @@ -933,16 +1156,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp get numCols() { return NumCast(this.document.gridNumCols, 10); } /** - * Sets the value of `numCols` on the grid's Document to the value entered. - */ - @undoBatch - onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter" || e.key === "Tab") { - if (e.currentTarget.valueAsNumber > 0) { - this.document.gridNumCols = e.currentTarget.valueAsNumber; - } - - } + * Sets the value of `numCols` on the grid's Document to the value entered. + */ + onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.currentTarget.valueAsNumber > 0) undoBatch(() => this.document.gridNumCols = e.currentTarget.valueAsNumber)(); } /** @@ -980,9 +1197,10 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp */ onDecrementButtonClick = () => { this.clicked = true; - if (!this.decrementLimitReached) { + if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; undoBatch(() => this.document.gridNumCols = this.numCols - 1)(); + if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; } @@ -1005,7 +1223,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp decrementValue = () => { this.entered = true; if (!this.clicked) { - if (this.numCols !== 1) { + if (this.numCols > 1) { this.document.gridNumCols = this.numCols - 1; } else { @@ -1038,9 +1256,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp <span className="grid-icon"> <FontAwesomeIcon icon="columns" size="1x" /> </span> - <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> - <input className="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> - <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> + <input className="collectionGridViewChrome-entryBox" type="number" value={this.numCols} onChange={this.onNumColsChange} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> + <input className="collectionGridViewChrome-columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> + <input className="collectionGridViewChrome-columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> </span> {/* <span className="grid-control"> <span className="grid-icon"> @@ -1081,4 +1299,15 @@ export class CollectionGridViewChrome extends React.Component<CollectionMenuProp </div> ); } -}
\ No newline at end of file +} +Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { + const dataField = doc[Doc.LayoutFieldKey(doc)]; + const childDocs = DocListCast(dataField); + const currentFrame = Cast(doc._currentFrame, "number", null); + if (currentFrame === undefined) { + doc._currentFrame = 0; + CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); + } + CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); + doc._currentFrame = Math.max(0, newFrame); +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 2e4055256..522f93b88 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -28,10 +28,8 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); this.layoutDoc._chromeStatus = "disabled"; - this.layoutDoc.hideFilterView = true; } componentWillUnmount() { - this.layoutDoc.hideFilterView = false; this.layoutDoc._chromeStatus = this._originalChrome; } diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 20ce6b76d..18ae260e8 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,44 +1,38 @@ import React = require("react"); -import { action, observable, trace, computed, runInAction } from "mobx"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter, Utils, emptyPath } from "../../../Utils"; +import { DateField } from "../../../fields/DateField"; import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; -import { KeyCodes } from "../../util/KeyCodes"; -import { SetupDrag, DragManager } from "../../util/DragManager"; -import { CompileScript } from "../../util/Scripting"; -import { Transform } from "../../util/Transform"; -import { MAX_ROW_HEIGHT, COLLECTION_BORDER_WIDTH } from '../globalCssVariables.scss'; -import '../DocumentDecorations.scss'; -import { EditableView } from "../EditableView"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "./CollectionSchemaView.scss"; -import { CollectionView, Flyout } from "./CollectionView"; -import { NumCast, StrCast, BoolCast, FieldValue, Cast, DateCast } from "../../../fields/Types"; -import { Docs } from "../../documents/Documents"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faExpand } from '@fortawesome/free-solid-svg-icons'; +import { List } from "../../../fields/List"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; -import { SnappingManager } from "../../util/SnappingManager"; import { ComputedField } from "../../../fields/ScriptField"; +import { BoolCast, Cast, DateCast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; -import { List } from "../../../fields/List"; -import { OverlayView } from "../OverlayView"; -import { DocumentIconContainer } from "../nodes/DocumentIcon"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; -import { DateField } from "../../../fields/DateField"; -import { RichTextField } from "../../../fields/RichTextField"; +import { Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DocumentManager } from "../../util/DocumentManager"; +import { DragManager } from "../../util/DragManager"; +import { KeyCodes } from "../../util/KeyCodes"; +import { CompileScript } from "../../util/Scripting"; import { SearchUtil } from "../../util/SearchUtil"; +import { SnappingManager } from "../../util/SnappingManager"; +import { undoBatch } from "../../util/UndoManager"; +import '../DocumentDecorations.scss'; +import { EditableView } from "../EditableView"; +import { MAX_ROW_HEIGHT } from '../globalCssVariables.scss'; +import { DocumentIconContainer } from "../nodes/DocumentIcon"; +import { OverlayView } from "../OverlayView"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "./CollectionView"; const path = require('path'); -library.add(faExpand); - export interface CellProps { row: number; col: number; @@ -64,37 +58,24 @@ export interface CellProps { @observer export class CollectionSchemaCell extends React.Component<CellProps> { + public static resolvedFieldKey(column: string, rowDoc: Doc) { + const fieldKey = column; + if (fieldKey.startsWith("*")) { + const rootKey = fieldKey.substring(1); + const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))]; + const matchedKeys = allKeys.filter(key => key.includes(rootKey)); + if (matchedKeys.length) return matchedKeys[0]; + } + return fieldKey; + } @observable protected _isEditing: boolean = false; protected _focusRef = React.createRef<HTMLDivElement>(); - protected _document = this.props.rowProps.original; + protected _rowDoc = this.props.rowProps.original; protected _dropDisposer?: DragManager.DragDropDisposer; - - async componentWillMount() { - - } - - async componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - console.log("mounted"); - console.log(this.type); - if (this.type === "context") { - console.log("mounted2"); - const doc = Doc.GetProto(this.props.rowProps.original); - const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); - if (aliasdoc.length > 0) { - const targetContext = Cast(aliasdoc[0].context, Doc) as Doc; - console.log(StrCast(targetContext.title)); - runInAction(() => this.contents = StrCast(targetContext.title)); - } - } - - } - @observable contents: string = ""; - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } + componentDidMount() { document.addEventListener("keydown", this.onKeyDown); } + componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); } @action onKeyDown = (e: KeyboardEvent): void => { @@ -116,7 +97,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { - + this.onItemDown(e); this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); @@ -130,61 +111,39 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } catch { } } - // this._isEditing = true; - // this.props.setIsEditing(true); - - const field = this.props.rowProps.original[this.props.rowProps.column.id!]; - const doc = FieldValue(Cast(field, Doc)); - if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); + const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null); + doc && this.props.setPreviewDoc(doc); } @undoBatch applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) }); if (!res.success) return false; - // doc[this.props.fieldKey] = res.result; - // return true; - doc[this.props.rowProps.column.id as string] = res.result; + doc[this.renderFieldKey] = res.result; return true; } private drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - const fieldKey = this.props.rowProps.column.id as string; if (de.complete.docDragData.draggedDocuments.length === 1) { - this._document[fieldKey] = de.complete.docDragData.draggedDocuments[0]; + this._rowDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0]; } else { const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {}); - this._document[fieldKey] = coll; + this._rowDoc[this.renderFieldKey] = coll; } e.stopPropagation(); } } protected dropRef = (ele: HTMLElement | null) => { - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); - } + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } - // expandDoc = (e: React.PointerEvent) => { - // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; - // let doc = FieldValue(Cast(field, Doc)); - - // this.props.setPreviewDoc(doc!); - - // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - - // e.stopPropagation(); - // } - - returnHighlights(bing: (() => string), positions?: number[]) { - const results = []; - const contents = bing(); - - if (positions !== undefined) { + returnHighlights(contents: string, positions?: number[]) { + if (positions) { + const results = []; StrCast(this.props.Document._searchString); const length = StrCast(this.props.Document._searchString).length; const color = contents ? "black" : "grey"; @@ -199,71 +158,24 @@ export class CollectionSchemaCell extends React.Component<CellProps> { ); return results; } - else { - return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; - } + return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>; } - type: string = ""; + @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); } + onItemDown = async (e: React.PointerEvent) => { + if (this.props.Document._searchDoc) { + const doc = Doc.GetProto(this._rowDoc); + const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); + const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null); + DocumentManager.Instance.jumpToDocument(this._rowDoc, false, () => undefined, targetContext); + } + } renderCellWithType(type: string | undefined) { const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - docFilters: returnEmptyFilter, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne - }; - - let matchedKeys = [props.fieldKey]; - if (props.fieldKey.startsWith("*")) { - const allKeys = Array.from(Object.keys(props.Document)); - allKeys.push(...Array.from(Object.keys(Doc.GetProto(props.Document)))); - matchedKeys = allKeys.filter(key => key.includes(props.fieldKey.substring(1))); - } - const fieldKey = matchedKeys.length ? matchedKeys[0] : props.fieldKey; - const field = props.Document[fieldKey]; - const doc = FieldValue(Cast(field, Doc)); - const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); + const fieldKey = this.renderFieldKey; + const field = this._rowDoc[fieldKey]; - const onItemDown = async (e: React.PointerEvent) => { - if (this.props.Document._searchDoc !== undefined) { - const doc = Doc.GetProto(this.props.rowProps.original); - const aliasdoc = await SearchUtil.GetAliasesOfDocument(doc); - let targetContext = undefined; - if (aliasdoc.length > 0) { - targetContext = Cast(aliasdoc[0].context, Doc) as Doc; - } - DocumentManager.Instance.jumpToDocument(this.props.rowProps.original, false, undefined, targetContext); - } - else { - fieldIsDoc && - SetupDrag(this._focusRef, - () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document, - this._document[props.fieldKey] instanceof Doc ? (doc: Doc | Doc[], target: Doc | undefined, addDoc: (newDoc: Doc | Doc[]) => any) => addDoc(doc) : this.props.moveDocument, - this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e); - } - }; const onPointerEnter = (e: React.PointerEvent): void => { if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) { dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over"; @@ -273,52 +185,24 @@ export class CollectionSchemaCell extends React.Component<CellProps> { dragRef.current!.className = "collectionSchemaView-cellContainer"; }; - let contents: any = "incorrect type"; - if (type === undefined) contents = <FieldView {...props} fieldKey={fieldKey} />; - if (type === "number") contents = typeof field === "number" ? NumCast(field) : "--" + typeof field + "--"; - if (type === "string") contents = typeof field === "string" ? (StrCast(field) === "" ? "--" : StrCast(field)) : "--" + typeof field + "--"; - if (type === "boolean") contents = typeof field === "boolean" ? (BoolCast(field) ? "true" : "false") : "--" + typeof field + "--"; - if (type === "document") { - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "image") { - const image = FieldValue(Cast(field, ImageField)); - const doc = FieldValue(Cast(field, Doc)); - contents = typeof field === "object" ? doc ? StrCast(doc.title) === "" ? "--" : StrCast(doc.title) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "list") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - if (type === "date") { - contents = typeof field === "object" ? doc ? StrCast(field) === "" ? "--" : StrCast(field) : `--${typeof field}--` : `--${typeof field}--`; - } - + let contents = Field.toString(field as Field); + contents = contents === "" ? "--" : contents; let className = "collectionSchemaView-cellWrapper"; if (this._isEditing) className += " editing"; if (this.props.isFocused && this.props.isEditable) className += " focused"; if (this.props.isFocused && !this.props.isEditable) className += " inactive"; - - // let docExpander = ( - // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} > - // <FontAwesomeIcon icon="expand" size="sm" /> - // </div> - // ); const positions = []; - let cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - this.type = props.fieldKey; if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { - let term = Field.toString(cfield as Field); - term = term.toLowerCase(); + let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase(); const search = StrCast(this.props.Document._searchString).toLowerCase(); let start = term.indexOf(search); let tally = 0; if (start !== -1) { positions.push(start); } - while (start < contents.length && start !== -1) { + while (start < contents?.length && start !== -1) { term = term.slice(start + search.length + 1); tally += start + search.length + 1; start = term.indexOf(search); @@ -328,179 +212,94 @@ export class CollectionSchemaCell extends React.Component<CellProps> { positions.pop(); } } - let search = false; - if (this.props.Document._searchDoc !== undefined) { - search = true; - } - + const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined"; return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} + <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> - <div className={className} ref={this._focusRef} onPointerDown={onItemDown} tabIndex={-1}> - <div className="collectionSchemaView-cellContents" - ref={type === undefined || type === "document" ? this.dropRef : null} key={props.Document[Id]}> - {!search ? + <div className={className} ref={this._focusRef} tabIndex={-1}> + <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}> + {!this.props.Document._searchDoc ? <EditableView - positions={positions.length > 0 ? positions : undefined} - search={StrCast(this.props.Document._searchString) ? StrCast(this.props.Document._searchString) : undefined} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} - contents={contents ? contents : type === "number" ? "0" : "undefined"} - highlight={positions.length > 0 ? true : undefined} - //contents={StrCast(contents)} + contents={contents} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} - placeholder={"undefined"} - bing={() => { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (cfield !== undefined) { - // if (typeof(cfield)===RichTextField) - const a = cfield as RichTextField; - const b = cfield as DateField; - console.log(b); - if (a.Text !== undefined) { - return (a.Text); - } - else if (b.toString() !== undefined) { - return b.toString(); - } - else if (StrCast(cfield)) { - return StrCast(cfield); - } - else { - return String(NumCast(cfield)); - } - } - }} + placeholder={placeholder} GetValue={() => { - if (type === "number" && (contents === 0 || contents === "0")) { - return "0"; - } else { - const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - if (type === "number") { - return StrCast(cfield); - } - const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; - const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; - const val = cscript !== undefined ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : - Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; - return val; - - } - + const cfield = ComputedField.WithoutComputed(() => FieldValue(field)); + const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined; + const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1]; + return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : + Field.IsField(cfield) ? Field.toScriptString(cfield) : ""; }} SetValue={action((value: string) => { let retVal = false; - - if (value.startsWith(":=")) { - retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + if (value.startsWith(":=") || value.startsWith("=:=")) { + const script = value.substring(value.startsWith("=:=") ? 3 : 2); + retVal = this.props.setComputed(script, value.startsWith(":=") ? Doc.GetProto(this.props.Document) : this.props.Document, this.renderFieldKey, this.props.row, this.props.col); } else { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (script.compiled) { - retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - } - + script.compiled && (retVal = this.applyToDoc(this._rowDoc, this.props.row, this.props.col, script.run)); } if (retVal) { this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing' this.props.setIsEditing(false); } return retVal; - - //return true; })} OnFillDown={async (value: string) => { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (script.compiled) { - DocListCast(this.props.Document[this.props.fieldKey]). - forEach((doc, i) => value.startsWith(":=") ? - this.props.setComputed(value.substring(2), doc, this.props.rowProps.column.id!, i, this.props.col) : - this.applyToDoc(doc, i, this.props.col, script.run)); - } + script.compiled && DocListCast(field). + forEach((doc, i) => value.startsWith(":=") ? + this.props.setComputed(value.substring(2), doc, this.renderFieldKey, i, this.props.col) : + this.applyToDoc(doc, i, this.props.col, script.run)); }} /> : - this.returnHighlights(() => { - console.log(props.fieldKey); - const dateCheck: Date | undefined = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : undefined; - if (dateCheck !== undefined) { - cfield = dateCheck.toLocaleString(); - } - if (props.fieldKey === "context") { - cfield = this.contents; - console.log("this should work"); - } - if (props.fieldKey === "*lastModified") { - if (FieldValue(props.Document["data-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["data-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - - else if (FieldValue(props.Document["text-lastModified"]) !== undefined) { - const d = ComputedField.WithoutComputed(() => FieldValue(props.Document["text-lastModified"])) as DateField; - cfield = d.date.toLocaleString(); - } - } - return Field.toString(cfield as Field); - }, positions) + this.returnHighlights(contents, positions) } </div > - {/* {fieldIsDoc ? docExpander : null} */} </div> </div> ); } - render() { - return this.renderCellWithType(undefined); - } + render() { return this.renderCellWithType(undefined); } } @observer -export class CollectionSchemaNumberCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("number"); - } -} +export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } } @observer -export class CollectionSchemaBooleanCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("boolean"); - } -} +export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } } @observer -export class CollectionSchemaStringCell extends CollectionSchemaCell { - render() { - return this.renderCellWithType("string"); - } -} +export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } } @observer export class CollectionSchemaDateCell extends CollectionSchemaCell { - @observable private _date: Date = this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof DateField ? DateCast(this.props.rowProps.original[this.props.rowProps.column.id as string]).date : - this.props.rowProps.original[this.props.rowProps.column.id as string] instanceof Date ? this.props.rowProps.original[this.props.rowProps.column.id as string] : new Date(); + @computed get _date(): Opt<DateField> { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; } @action handleChange = (date: any) => { - this._date = date; // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to - this._document[this.props.rowProps.column.id as string] = date as Date; + this._rowDoc[this.renderFieldKey] = new DateField(date as Date); //} } render() { - return <DatePicker - selected={this._date} - onSelect={date => this.handleChange(date)} - onChange={date => this.handleChange(date)} - />; + return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> : + <DatePicker + selected={this._date?.date || new Date} + onSelect={date => this.handleChange(date)} + onChange={date => this.handleChange(date)} + />; } } @@ -509,44 +308,11 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _doc = FieldValue(Cast(this._field, Doc)); - @observable private _docTitle = this._doc?.title; - @observable private _preview = false; - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableWidth() { return this.prop.PanelWidth() - 2 * this.borderWidth - 4 - this.previewWidth(); } + @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); } @action onSetValue = (value: string) => { - this._docTitle = value; - //this.prop.Document[this.prop.fieldKey] = this._text; + this._doc && (Doc.GetProto(this._doc).title = value); const script = CompileScript(value, { addReturn: true, @@ -556,46 +322,22 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const results = script.compiled && script.run(); if (results && results.success) { - this._doc = results.result; - this._document[this.prop.fieldKey] = results.result; - this._docTitle = this._doc?.title; - + this._rowDoc[this.renderFieldKey] = results.result; return true; } return false; } + componentWillUnmount() { this.onBlur(); } + + onBlur = () => { this._overlayDisposer?.(); }; onFocus = () => { - this._overlayDisposer?.(); + this.onBlur(); this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } @action - onOpenClick = () => { - this._preview = false; - if (this._doc) { - this.props.addDocTab(this._doc, "onRight"); - return true; - } - return false; - } - - @action - showPreview = (bool: boolean, e: any) => { - if (this._isEditing) { - this._preview = false; - } else { - if (bool) { - this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY); - } else { - this.props.showDoc(undefined); - } - } - } - - @action - isEditingCalling = (isEditing: boolean): void => { - this.showPreview(false, ""); + isEditingCallback = (isEditing: boolean): void => { document.removeEventListener("keydown", this.onKeyDown); isEditing && document.addEventListener("keydown", this.onKeyDown); this._isEditing = isEditing; @@ -603,227 +345,90 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { this.props.changeFocusedCellByIndex(this.props.row, this.props.col); } - onDown = (e: any) => { - this.props.changeFocusedCellByIndex(this.props.row, this.props.col); - this.props.setPreviewDoc(this.props.rowProps.original); - - let url: string; - if (url = StrCast(this.props.rowProps.row.href)) { - try { - new URL(url); - const temp = window.open(url)!; - temp.blur(); - window.focus(); - } catch { } - } - - const field = this.props.rowProps.original[this.props.rowProps.column.id!]; - const doc = FieldValue(Cast(field, Doc)); - if (typeof field === "object" && doc) this.props.setPreviewDoc(doc); - - this.showPreview(true, e); - - } - render() { - if (typeof this._field === "object" && this._doc && this._docTitle) { - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} - onPointerDown={(e) => { this.onDown(e); }} - onPointerEnter={(e) => { this.showPreview(true, e); }} - onPointerLeave={(e) => { this.showPreview(false, e); }} + return !this._doc ? this.renderCellWithType("document") : + <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} + onPointerDown={this.onPointerDown} + > + <div className="collectionSchemaView-cellContents-document" + style={{ padding: "5.9px" }} + ref={this.dropRef} + onFocus={this.onFocus} + onBlur={this.onBlur} > - - <div className="collectionSchemaView-cellContents-document" - style={{ padding: "5.9px" }} - ref={this.dropRef} - onFocus={this.onFocus} - onBlur={() => this._overlayDisposer?.()} - > - - <EditableView - editing={this._isEditing} - isEditingCallback={this.isEditingCalling} - display={"inline"} - contents={this._docTitle} - height={"auto"} - maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return StrCast(this._docTitle); - }} - SetValue={action((value: string) => { - this.onSetValue(value); - this.showPreview(false, ""); - return true; - })} - /> - </div > - <div onClick={this.onOpenClick} className="collectionSchemaView-cellContents-docButton"> - <FontAwesomeIcon icon="external-link-alt" size="lg" ></FontAwesomeIcon> </div> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={"inline"} + contents={this._doc.title || "--"} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => StrCast(this._doc?.title)} + SetValue={action((value: string) => { + this.onSetValue(value); + return true; + })} + /> + </div > + <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton"> + <FontAwesomeIcon icon="external-link-alt" size="lg" /> </div> - ); - } else { - return this.renderCellWithType("document"); - } + </div>; } } @observer export class CollectionSchemaImageCell extends CollectionSchemaCell { - // render() { - // return this.renderCellWithType("image"); - // } - choosePath(url: URL, dataDoc: any) { - const lower = url.href.toLowerCase(); - if (url.protocol === "data") { - return url.href; - } else if (url.href.indexOf(window.location.origin) === -1) { - return Utils.CorsProxy(url.href); - } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) { - return url.href;//Why is this here - } + choosePath(url: URL) { + if (url.protocol === "data") return url.href; + if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here + const ext = path.extname(url.href); - const _curSuffix = "_o"; - return url.href.replace(ext, _curSuffix + ext); + return url.href.replace(ext, "_o" + path.extname(url.href)); } render() { - const props: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - - let image = true; - let url = []; - if (props.DataDoc) { - const field = Cast(props.DataDoc[props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(props.DataDoc[props.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url, props.DataDoc)); // access the primary layout data of the alternate documents - const paths = field ? [this.choosePath(field.url, props.DataDoc), ...altpaths] : altpaths; - if (paths.length) { - url = paths; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } - //url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - } else { - url = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - image = false; - } - - const heightToWidth = NumCast(props.DataDoc?._nativeHeight) / NumCast(props.DataDoc?._nativeWidth); - const height = this.props.rowProps.width * heightToWidth; + const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc + const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images + const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; + const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; - if (props.fieldKey === "data") { - if (url !== []) { - const reference = React.createRef<HTMLDivElement>(); - return ( - <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> - <img src={url[0]} width={image ? this.props.rowProps.width : "30px"} - height={image ? height : "30px"} /> - </div > - </div> - ); + const heightToWidth = NumCast(this._rowDoc._nativeHeight) / NumCast(this._rowDoc._nativeWidth); + let width = Math.min(75, this.props.rowProps.width); + const height = Math.min(75, width * heightToWidth); + width = height / heightToWidth; - } else { - return this.renderCellWithType("image"); - } - } else { - return this.renderCellWithType("image"); - } + const reference = React.createRef<HTMLDivElement>(); + return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> + <img src={url[0]} + width={paths.length ? width : "20px"} + height={paths.length ? height : "20px"} /> + </div > + </div>; } } - - - @observer export class CollectionSchemaListCell extends CollectionSchemaCell { - _overlayDisposer?: () => void; - private prop: FieldViewProps = { - Document: this.props.rowProps.original, - DataDoc: this.props.rowProps.original, - LibraryPath: [], - dropAction: "alias", - bringToFront: emptyFunction, - rootSelected: returnFalse, - fieldKey: this.props.rowProps.column.id as string, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document, - isSelected: returnFalse, - select: emptyFunction, - renderDepth: this.props.renderDepth + 1, - ScreenToLocalTransform: Transform.Identity, - focus: emptyFunction, - active: returnFalse, - whenActiveChanged: emptyFunction, - PanelHeight: returnZero, - PanelWidth: returnZero, - NativeHeight: returnZero, - NativeWidth: returnZero, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - ContentScaling: returnOne, - docFilters: returnEmptyFilter - }; - @observable private _field = this.prop.Document[this.prop.fieldKey]; - @observable private _optionsList = this._field as List<any>; + @computed get _field() { return this._rowDoc[this.renderFieldKey]; } + @computed get _optionsList() { return this._field as List<any>; } @observable private _opened = false; @observable private _text = "select an item"; @observable private _selectedNum = 0; @action - toggleOpened(open: boolean) { - this._opened = open; - } - - // @action - // onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - // this._text = e.target.value; - - // // change if its a document - // this._optionsList[this._selectedNum] = this._text; - // } - - @action onSetValue = (value: string) => { - - - this._text = value; - // change if its a document - this._optionsList[this._selectedNum] = this._text; - - (this.prop.Document[this.prop.fieldKey] as List<any>).splice(this._selectedNum, 1, value); + this._optionsList[this._selectedNum] = this._text = value; + (this._field as List<any>).splice(this._selectedNum, 1, value); } @action @@ -837,55 +442,23 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); } - render() { - - const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); - - let type = "list"; - - let link = false; - let doc = false; + const link = false; const reference = React.createRef<HTMLDivElement>(); - if (typeof this._field === "object" && this._optionsList[0]) { - - const options = this._optionsList.map((element, index) => { - - if (element instanceof Doc) { - doc = true; - type = "document"; - if (this.prop.fieldKey.toLowerCase() === "links") { - link = true; - type = "link"; - } - const document = FieldValue(Cast(element, Doc)); - const title = element.title; - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element.title), index); }} - style={{ padding: "6px" }}> - {title} - </div>; - - } else { - return <div - className="collectionSchemaView-dropdownOption" - onPointerDown={(e) => { this.onSelected(StrCast(element), index); }} - style={{ padding: "6px" }}>{element}</div>; - } - }); + if (this._optionsList?.length) { + const options = !this._opened ? (null) : + <div> + {this._optionsList.map((element, index) => { + const val = Field.toString(element); + return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} > + {val} + </div>; + })} + </div>; const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>; - // const textarea = <textarea onChange={this.onChange} value={this._text} - // onFocus={doc ? this.onFocus : undefined} - // onBlur={doc ? e => this._overlayDisposer?.() : undefined} - // style={{ resize: "none" }} - // placeholder={"select an item"}></textarea>; - - const textarea = <div className="collectionSchemaView-cellContents" - style={{ padding: "5.9px" }} - ref={type === undefined || type === "document" ? this.dropRef : null} key={this.prop.Document[Id]}> + const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} > <EditableView editing={this._isEditing} isEditingCallback={this.isEditingCallback} @@ -893,11 +466,8 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { contents={this._text} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} - GetValue={() => { - return this._text; - }} + GetValue={() => this._text} SetValue={action((value: string) => { - // add special for params this.onSetValue(value); return true; @@ -906,61 +476,35 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { </div >; //☰ - - const dropdown = <div> - {options} - </div>; - return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" key={this._document[Id]} ref={reference}> + <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}> <div className="collectionSchemaView-dropDownWrapper"> - <button type="button" className="collectionSchemaView-dropdownButton" onClick={(e) => { this.toggleOpened(!this._opened); }} - style={{ right: "length", position: "relative" }}> - {this._opened ? <FontAwesomeIcon icon="caret-up" size="lg" ></FontAwesomeIcon> - : <FontAwesomeIcon icon="caret-down" size="lg" ></FontAwesomeIcon>} + <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }} + onClick={action(e => this._opened = !this._opened)} > + <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" /> </button> <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div> </div> - - {this._opened ? dropdown : null} + {options} </div > </div> ); - } else { - return this.renderCellWithType("list"); } + return this.renderCellWithType("list"); } } - - - @observer export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { - @observable private _isChecked: boolean = typeof this.props.rowProps.original[this.props.rowProps.column.id as string] === "boolean" ? BoolCast(this.props.rowProps.original[this.props.rowProps.column.id as string]) : false; - - @action - toggleChecked = (e: React.ChangeEvent<HTMLInputElement>) => { - this._isChecked = e.target.checked; - const script = CompileScript(e.target.checked.toString(), { requiredType: "boolean", addReturn: true, params: { this: Doc.name } }); - if (script.compiled) { - this.applyToDoc(this._document, this.props.row, this.props.col, script.run); - } - } + @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); } render() { const reference = React.createRef<HTMLDivElement>(); - const onItemDown = (e: React.PointerEvent) => { - (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : - SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); - }; return ( <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}> - <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={this._document[Id]} ref={reference}> - <input type="checkbox" checked={this._isChecked} onChange={this.toggleChecked} /> - </div > + <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} /> </div> ); } @@ -969,62 +513,15 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell { @observer export class CollectionSchemaButtons extends CollectionSchemaCell { - render() { - // const reference = React.createRef<HTMLDivElement>(); - // const onItemDown = (e: React.PointerEvent) => { - // (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined : - // SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e)); - // }; - const doc = this.props.rowProps.original; - let buttons: JSX.Element | undefined = undefined; - buttons = <div style={{ - paddingTop: 8, - paddingLeft: 3, - }}><button onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }} style={{ padding: 2, left: 77 }}> - <FontAwesomeIcon icon="arrow-up" size="sm" /> - </button> - <button onClick={() => { - { - doc.searchMatchAlt = false; - setTimeout(() => doc.searchMatchAlt = true, 0); - } - }} style={{ padding: 2 }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div>; - const type = StrCast(doc.type); - if (type === "pdf") { - buttons = <div><button - style={{ - position: "relative", - height: 30, - width: 28, - left: 1, - }} - - onClick={() => { - doc.searchMatch = false; - setTimeout(() => doc.searchMatch = true, 0); - }}> - <FontAwesomeIcon icon="arrow-down" size="sm" /> - </button></div >; - } - else if (type !== "rtf") { - buttons = undefined; - } - - if (BoolCast(this.props.Document._searchDoc) === true) { - - } - else { - buttons = undefined; - } - return ( - <div> {buttons}</div> - ); + return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> : + <div style={{ paddingTop: 8, paddingLeft: 3 }} > + <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}> + <FontAwesomeIcon icon="arrow-up" size="sm" /> + </button> + <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} > + <FontAwesomeIcon icon="arrow-down" size="sm" /> + </button> + </div>; } -} - +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index 5c0e6581b..d2a1234ed 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -13,57 +13,11 @@ import { SearchBox } from "../search/SearchBox"; import { ColumnType } from "./CollectionSchemaView"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; -import * as fa from '@fortawesome/free-solid-svg-icons'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -export interface HeaderProps { - keyValue: SchemaHeaderField; - possibleKeys: string[]; - existingKeys: string[]; - keyType: ColumnType; - typeConst: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; - setIsEditing: (isEditing: boolean) => void; - deleteColumn: (column: string) => void; - setColumnType: (column: SchemaHeaderField, type: ColumnType) => void; - setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void; - setColumnColor: (column: SchemaHeaderField, color: string) => void; - -} - -export class CollectionSchemaHeader extends React.Component<HeaderProps> { - render() { - const icon: IconProp = this.props.keyType === ColumnType.Number ? "hashtag" : this.props.keyType === ColumnType.String ? "font" : - this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "sort-amount-down" : - this.props.keyType === ColumnType.Image ? "image" : this.props.keyType === ColumnType.List ? "list-ul" : this.props.keyType === ColumnType.Date ? "calendar" : - "align-justify"; - return ( - <div className="collectionSchemaView-header" style={{ background: this.props.keyValue.color }}> - <CollectionSchemaColumnMenu - columnField={this.props.keyValue} - // keyValue={this.props.keyValue.heading} - possibleKeys={this.props.possibleKeys} - existingKeys={this.props.existingKeys} - // keyType={this.props.keyType} - typeConst={this.props.typeConst} - menuButtonContent={<div><FontAwesomeIcon icon={icon} size="sm" />{this.props.keyValue.heading}</div>} - addNew={false} - onSelect={this.props.onSelect} - setIsEditing={this.props.setIsEditing} - deleteColumn={this.props.deleteColumn} - onlyShowOptions={false} - setColumnType={this.props.setColumnType} - setColumnSort={this.props.setColumnSort} - setColumnColor={this.props.setColumnColor} - /> - </div> - ); - } -} - export interface AddColumnHeaderProps { createColumn: () => void; @@ -79,7 +33,6 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe } - export interface ColumnMenuProps { columnField: SchemaHeaderField; // keyValue: string; @@ -103,37 +56,29 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> @observable private _isOpen: boolean = false; @observable private _node: HTMLDivElement | null = null; - componentDidMount() { - document.addEventListener("pointerdown", this.detectClick); - } + componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } - componentWillUnmount() { - document.removeEventListener("pointerdown", this.detectClick); - } + componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } - detectClick = (e: PointerEvent): void => { - if (this._node && this._node.contains(e.target as Node)) { - } else { - this._isOpen = false; - this.props.setIsEditing(false); - } + @action + detectClick = (e: PointerEvent) => { + !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false); } @action toggleIsOpen = (): void => { - this._isOpen = !this._isOpen; - this.props.setIsEditing(this._isOpen); + this.props.setIsEditing(this._isOpen = !this._isOpen); } - changeColumnType = (type: ColumnType): void => { + changeColumnType = (type: ColumnType) => { this.props.setColumnType(this.props.columnField, type); } - changeColumnSort = (desc: boolean | undefined): void => { + changeColumnSort = (desc: boolean | undefined) => { this.props.setColumnSort(this.props.columnField, desc); } - changeColumnColor = (color: string): void => { + changeColumnColor = (color: string) => { this.props.setColumnColor(this.props.columnField, color); } @@ -145,7 +90,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } renderTypes = () => { - if (this.props.typeConst) return <></>; + if (this.props.typeConst) return (null); const type = this.props.columnField.type; return ( @@ -291,7 +236,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable private _key: string = this.props.keyValue; @observable private _searchTerm: string = this.props.keyValue; @observable private _isOpen: boolean = false; - @observable private _canClose: boolean = true; + @observable private _node: HTMLDivElement | null = null; @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef(); @action setSearchTerm = (value: string): void => { this._searchTerm = value; }; @@ -306,6 +251,31 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { this.props.setIsEditing(false); } + @action + setNode = (node: HTMLDivElement): void => { + if (node) { + this._node = node; + } + } + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters?.includes(this._key)) { + runInAction(() => this.closeResultsVisibility = "contents"); + } + } + + @action + detectClick = (e: PointerEvent): void => { + if (this._node && this._node.contains(e.target as Node)) { + } else { + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + private tempfilter: string = ""; @undoBatch onKeyDown = (e: React.KeyboardEvent): void => { if (e.key === "Enter") { @@ -313,23 +283,27 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (temp === "") { - Doc.setDocFilter(this.props.Document, this._key, temp, undefined); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); } else { - Doc.setDocFilter(this.props.Document, this._key, temp, "match"); + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.tempfilter = temp; + Doc.setDocFilter(this.props.Document, this._key, temp, "check"); this.props.col.setColor("green"); + this.closeResultsVisibility = "contents"; } } else { + Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, undefined); + this.updateFilter(); let keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); - const blockedkeys = ["_scrollTop", "customTitle", "limitHeight", "proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); + const blockedkeys = ["system", "title-custom", "limitHeight", "proto", "x", "y", "zIndex", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "layout", "layout_keyValue", "links"]; + keyOptions = keyOptions.filter(n => !blockedkeys.includes(n) && !n.startsWith("_") && !n.startsWith("ACL")); if (keyOptions.length) { this.onSelect(keyOptions[0]); - console.log("case1"); } else if (this._searchTerm !== "" && this.props.canAddNew) { this.setSearchTerm(this._searchTerm || this._key); - console.log("case2"); this.onSelect(this._searchTerm); } } @@ -347,23 +321,6 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { } @action - onBlur = (e: React.FocusEvent): void => { - if (this._canClose) { - this._isOpen = false; - this.props.setIsEditing(false); - } - } - - @action - onPointerEnter = (e: React.PointerEvent): void => { - this._canClose = false; - } - - @action - onPointerOut = (e: React.PointerEvent): void => { - this._canClose = true; - } - @action renderOptions = (): JSX.Element[] | JSX.Element => { if (!this._isOpen) { this.defaultMenuHeight = 0; @@ -375,21 +332,26 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 || this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1; - const blockedkeys = ["proto", "x", "y", "_width", "_height", "_autoHeight", "_fontSize", "_fontFamily", "context", "zIndex", "_timeStampOnEnter", "lines", "highlighting", "searchMatch", "creationDate", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "_xMargin", "_yMargin", "layout", "layout_keyValue", "links"]; - keyOptions = keyOptions.filter(n => !blockedkeys.includes(n)); + const blockedkeys = ["proto", "x", "y", "zIndex", "_timeStampOnEnter", "isPrototype", "text-annotations", "aliases", "text-lastModified", "text-noTemplate", "layoutKey", "baseProto", "layout", "layout_keyValue", "links"]; + keyOptions = keyOptions.filter(n => !blockedkeys.includes(n) && !n.startsWith("_") && !n.startsWith("ACL")); const options = keyOptions.map(key => { return <div key={key} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", }} - onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + onPointerDown={e => { + e.stopPropagation(); + }} + onClick={() => { + this.onSelect(key); + this.setSearchTerm(""); + }}>{key}</div>; }); // if search term does not already exist as a group type, give option to create new group type if (this._key !== this._searchTerm.slice(0, this._key.length)) { - console.log("little further"); if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { options.push(<div key={""} className="key-option" style={{ border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", @@ -418,55 +380,55 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @action renderFilterOptions = (): JSX.Element[] | JSX.Element => { - if (!this._isOpen) { + if (!this._isOpen || !this.props.dataDoc) { this.defaultMenuHeight = 0; return <></>; } - const keyOptions: string[] = []; const colpos = this._searchTerm.indexOf(":"); const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length); if (this.docSafe.length === 0) { - this.docSafe = DocListCast(this.props.dataDoc![this.props.fieldKey]); + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); } const docs = this.docSafe; docs.forEach((doc) => { const key = StrCast(doc[this._key]); - if (keyOptions.includes(key) === false && key.includes(temp)) { + if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") { keyOptions.push(key); } }); const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } for (let i = 0; i < (filters?.length ?? 0) - 1; i += 3) { if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i + 1]) === false) { keyOptions.push(filters![i + 1]); } } - - if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { - this.props.col.setColor("rgb(241, 239, 235)"); - } - const options = keyOptions.map(key => { - //Doc.setDocFilter(this.props.Document!, this._key, key, undefined); let bool = false; - console.log(filters); if (filters !== undefined) { bool = filters.includes(key) && filters[filters.indexOf(key) + 1] === "check"; - console.log(filters.includes(key)); } return <div key={key} className="key-option" style={{ border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", }} > - <input type="checkbox" onChange={(e) => { - e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); - e.target.checked === true ? this.props.col.setColor("green") : ""; - e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); - }} - checked={bool} ></input> + <input type="checkbox" + onPointerDown={e => e.stopPropagation()} + onClick={e => e.stopPropagation()} + onChange={(e) => { + e.target.checked === true ? Doc.setDocFilter(this.props.Document, this._key, key, "check") : Doc.setDocFilter(this.props.Document, this._key, key, undefined); + e.target.checked === true ? this.closeResultsVisibility = "contents" : console.log(""); + e.target.checked === true ? this.props.col.setColor("green") : this.updateFilter(); + e.target.checked === true && SearchBox.Instance.filter === true ? Doc.setDocFilter(docs[0], this._key, key, "check") : Doc.setDocFilter(docs[0], this._key, key, undefined); + }} + checked={bool} + /> <span style={{ paddingLeft: 4 }}> {key} </span> @@ -492,41 +454,67 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { @observable defaultMenuHeight = 0; + updateFilter() { + const filters = Cast(this.props.Document._docFilters, listSpec("string")); + if (filters === undefined || filters.length === 0 || filters.includes(this._key) === false) { + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } + } + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } @computed get scriptField() { - console.log("we kinda made it"); const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); return script ? () => script : undefined; } filterBackground = () => "rgba(105, 105, 105, 0.432)"; - @observable filterOpen: boolean | undefined = undefined; + closeResultsVisibility: string = "none"; + + removeFilters = (e: React.PointerEvent): void => { + const keyOptions: string[] = []; + if (this.docSafe.length === 0 && this.props.dataDoc) { + this.docSafe = DocListCast(this.props.dataDoc[this.props.fieldKey]); + } + const docs = this.docSafe; + docs.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false) { + keyOptions.push(key); + } + }); + + Doc.setDocFilter(this.props.Document, this._key, "", "remove"); + this.props.col.setColor("rgb(241, 239, 235)"); + this.closeResultsVisibility = "none"; + } render() { return ( - <div style={{ display: "flex" }}> - <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> + <div style={{ display: "flex" }} ref={this.setNode}> + <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) }} /> */} - <div className="keys-dropdown" style={{ zIndex: 10, width: this.props.width, maxWidth: this.props.width }}> + <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}> <input className="keys-search" style={{ width: "100%" }} ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} onChange={e => this.onChange(e.target.value)} - onClick={(e) => { - //this._inputRef.current!.select(); - e.stopPropagation(); - }} onFocus={this.onFocus} onBlur={this.onBlur}></input> - <div className="keys-options-wrapper" style={{ + onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }} + onFocus={this.onFocus} ></input> + <div style={{ display: this.closeResultsVisibility }}> + <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg" + style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} /> + </div> + {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{ width: this.props.width, maxWidth: this.props.width, height: "auto", - }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> + }}> {this._searchTerm.includes(":") ? this.renderFilterOptions() : this.renderOptions()} - </div> + </div>} </div > </div> ); diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index dade4f2f2..881246bd4 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -1,21 +1,17 @@ import React = require("react"); -import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table"; -import "./CollectionSchemaView.scss"; -import { Transform } from "../../util/Transform"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action } from "mobx"; +import { ReactTableDefaults, RowInfo, TableCellRenderer } from "react-table"; import { Doc } from "../../../fields/Doc"; -import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { Cast, FieldValue, StrCast } from "../../../fields/Types"; -import { ContextMenu } from "../ContextMenu"; -import { action } from "mobx"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faGripVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { DocumentManager } from "../../util/DocumentManager"; -import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { undoBatch } from "../../util/UndoManager"; +import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; import { SnappingManager } from "../../util/SnappingManager"; - -library.add(faGripVertical, faTrash); +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import "./CollectionSchemaView.scss"; export interface MovableColumnProps { columnRenderer: TableCellRenderer; @@ -40,7 +36,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { onPointerLeave = (e: React.PointerEvent): void => { this._header!.current!.className = "collectionSchema-col-wrapper"; document.removeEventListener("pointermove", this.onDragMove, true); - document.removeEventListener("pointermove", this.onPointerMove); + !e.buttons && document.removeEventListener("pointermove", this.onPointerMove); } onDragMove = (e: PointerEvent): void => { const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); @@ -68,6 +64,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> { const before = x[0] < bounds[0]; const colDragData = de.complete.columnDragData; if (colDragData) { + e.stopPropagation(); this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); return true; } @@ -108,8 +105,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> { onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => { this._dragRef = ref; const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY); - this._startDragPosition = { x: dx, y: dy }; - document.addEventListener("pointermove", this.onPointerMove); + if (!(e.target as any)?.tagName.includes("INPUT")) { + this._startDragPosition = { x: dx, y: dy }; + document.addEventListener("pointermove", this.onPointerMove); + } } @@ -164,6 +163,10 @@ export class MovableRow extends React.Component<MovableRowProps> { if (!before) this._header!.current!.className += " row-below"; e.stopPropagation(); } + componentWillUnmount() { + + this._rowDropDisposer?.(); + } createRowDropTarget = (ele: HTMLDivElement) => { this._rowDropDisposer?.(); @@ -198,6 +201,7 @@ export class MovableRow extends React.Component<MovableRowProps> { } onRowContextMenu = (e: React.MouseEvent): void => { + e.preventDefault(); const description = this.props.rowWrapped ? "Unwrap text on row" : "Text wrap row"; ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: "file-pdf" }); } @@ -219,13 +223,15 @@ export class MovableRow extends React.Component<MovableRowProps> { render() { const { children = null, rowInfo } = this.props; + if (!rowInfo) { return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>; } const { original } = rowInfo; const doc = FieldValue(Cast(original, Doc)); - if (!doc) return <></>; + + if (!doc) return (null); const reference = React.createRef<HTMLDivElement>(); const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType); @@ -238,11 +244,11 @@ export class MovableRow extends React.Component<MovableRowProps> { <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > - {/* <div className="row-dragger"> - <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> - <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> - <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "onRight")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> - </div> */} + <div className="row-dragger"> + <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> + </div> {children} </ReactTableDefaults.TrComponent> </div> diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 5c2931a8b..8d2f645d9 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -59,6 +59,7 @@ justify-content: space-between; flex-wrap: nowrap; touch-action: none; + padding: 2px; div { touch-action: none; @@ -99,6 +100,9 @@ direction: ltr; overflow: visible; } + .rt-noData { + display: none; + } .rt-thead { width: 100%; @@ -139,6 +143,10 @@ width: 100%; direction: rtl; overflow: visible; + + .rt-td { + border-right: 1px solid rgba(0, 0, 0, 0.2); + } } .rt-tr-group { @@ -158,6 +166,8 @@ font-size: 13px; text-align: center; white-space: nowrap; + display: flex; + align-items: center; .imageBox-cont { position: relative; @@ -176,6 +186,11 @@ height: 100%; } } + .rt-td.rt-expandable { + display: flex; + align-items: center; + height: inherit; + } .rt-resizer { width: 8px; @@ -211,7 +226,8 @@ height: auto; z-index: 100; position: absolute; - background:white; + background: white; + padding: 5px; .collectionSchema-header-toggler { z-index: 100; @@ -243,12 +259,13 @@ button.add-column { .collectionSchema-header-menuOptions { color: black; - width: 200px; + width: 180px; text-align: left; .collectionSchema-headerMenu-group { padding: 7px 0; border-bottom: 1px solid lightgray; + cursor: pointer; &:first-child { padding-top: 0; @@ -326,6 +343,7 @@ button.add-column { background-color: white; border: 1px solid lightgray; padding: 2px 3px; + &:not(:first-child) { border-top: 0; } @@ -356,47 +374,6 @@ button.add-column { } } -.altcollectionTimeView-treeView { - display: flex; - flex-direction: column; - width: 175px; - height: auto; - position: fixed; - border-left: solid 1px; - z-index: 1; - - .collectionTimeView-addfacet { - display: inline-block; - width: 200px; - height: 30px; - background: darkGray; - text-align: left; - - .collectionTimeView-button { - align-items: center; - display: flex; - width: 100%; - height: 100%; - - .collectionTimeView-span { - margin: auto; - } - } - - >div, - >div>div { - width: 100%; - height: 100%; - } - } - - .altcollectionTimeView-tree { - display: inline-block; - width: 100%; - height: calc(100% - 30px); - } -} - .collectionSchema-row { height: 100%; background-color: white; @@ -414,11 +391,12 @@ button.add-column { .row-dragger { display: flex; justify-content: space-around; - flex: 50 0 auto; - width: 50px; + //flex: 50 0 auto; + width: 0; max-width: 50px; - height: 100%; + //height: 100%; min-height: 30px; + align-items: center; color: lightgray; background-color: white; transition: color 0.1s ease; @@ -426,10 +404,12 @@ button.add-column { .row-option { // padding: 5px; cursor: pointer; + position: absolute; transition: color 0.1s ease; display: flex; flex-direction: column; justify-content: center; + z-index: 2; &:hover { color: gray; @@ -459,15 +439,15 @@ button.add-column { .collectionSchemaView-cellContainer { width: 100%; - height: 100%; + height: unset; } .collectionSchemaView-cellWrapper { height: 100%; padding: 4px; - text-align:left; - padding-left:19px; - + text-align: left; + padding-left: 19px; + position: relative; &:focus { @@ -596,21 +576,23 @@ button.add-column { .collectionSchemaView-table { width: 100%; height: 100%; + overflow: auto; + padding: 3px; } +.rt-td.rt-expandable { + overflow: visible; + position: relative; + height:100%; + z-index: 1; +} .reactTable-sub { - padding: 10px 30px; background-color: rgb(252, 252, 252); width: 100%; .rt-thead { - display:none; - } - .collectionSchemaView-table{ - border: solid 1px; - overflow: hidden; + display: none; } - .row-dragger { background-color: rgb(252, 252, 252); @@ -621,20 +603,26 @@ button.add-column { } .collectionSchemaView-table { - width: 100%; + width: 100%; + border: solid 1px; + overflow: visible; + padding: 0px; } } .collectionSchemaView-expander { height: 100%; min-height: 30px; - position: relative; + position: absolute; color: gray; + width: 20; + height: auto; + left: 55; svg { position: absolute; top: 50%; - left: 50%; + left: 10; transform: translate(-50%, -50%); } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index f1de3cee7..1b68c0e1a 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,32 +1,29 @@ import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import { Resize } from "react-table"; import "react-table/react-table.css"; -import { Doc } from "../../../fields/Doc"; +import { Doc, Opt } from "../../../fields/Doc"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; -import { Cast, NumCast, BoolCast } from "../../../fields/Types"; +import { Cast, NumCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { SchemaTable } from "./SchemaTable"; - -library.add(faCog, faPlus, faSortUp, faSortDown); -library.add(faTable); // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -44,7 +41,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -62,8 +59,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable _menuWidth = 0; @observable _headerOpen = false; - @observable _isOpen = false; - @observable _node: HTMLDivElement | null = null; @observable _headerIsEditing = false; @observable _col: any = ""; @observable _menuHeight = 0; @@ -73,7 +68,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get menuCoordinates() { let searchx = 0; let searchy = 0; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { const el = document.getElementsByClassName("collectionSchemaView-searchContainer")[0]; if (el !== undefined) { const rect = el.getBoundingClientRect(); @@ -86,7 +81,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { return this.props.ScreenToLocalTransform().transformPoint(x, y); } - @observable scale = this.props.ScreenToLocalTransform().Scale; + @computed get scale() { return this.props.ScreenToLocalTransform().Scale; } @computed get columns() { return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); @@ -111,33 +106,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } @computed get possibleKeys() { return this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); } + @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; - componentDidMount() { - document.addEventListener("pointerdown", this.detectClick); - } - - componentWillUnmount() { - document.removeEventListener("pointerdown", this.detectClick); - } - - @action setHeaderIsEditing = (isEditing: boolean) => { - this._headerIsEditing = isEditing; - } - - detectClick = (e: PointerEvent): void => { - if (this._node && this._node.contains(e.target as Node)) { - } else { - this._isOpen = false; - this.setHeaderIsEditing(false); - this.closeHeader(); - } - } - - @action - toggleIsOpen = (): void => { - this._isOpen = !this._isOpen; - this.setHeaderIsEditing(this._isOpen); - } @action changeColumnType = (type: ColumnType, col: any): void => { @@ -190,16 +160,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; } - @action - setNode = (node: HTMLDivElement): void => { - node && (this._node = node); - } - - @action - typesDropdownChange = (bool: boolean) => { - this._openTypes = bool; - } - renderTypes = (col: any) => { if (columnTypes.get(col.heading)) return (null); @@ -263,10 +223,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { type === ColumnType.Date ? dateType : imageType; return ( - <div className="collectionSchema-headerMenu-group"> - <div onClick={() => this.typesDropdownChange(!this._openTypes)}> - <label>Column type:</label> - <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right" }} /> + <div className="collectionSchema-headerMenu-group" onClick={action(() => this._openTypes = !this._openTypes)}> + <div> + <label style={{ cursor: "pointer" }}>Column type:</label> + <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right", transform: `rotate(${this._openTypes ? "180deg" : 0})`, transition: "0.2s all ease" }} /> </div> {this._openTypes ? allColumnTypes : justColType} </div > @@ -340,17 +300,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.columns = columns; if (filter) { Doc.setDocFilter(this.props.Document, newKey, filter, "match"); - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - Doc.setDocFilter(doc, newKey, filter, "match"); - } } else { this.props.Document._docFilters = undefined; - if (this.props.Document.selectedDoc !== undefined) { - const doc = Cast(this.props.Document.selectedDoc, Doc) as Doc; - doc._docFilters = undefined; - } } } } @@ -360,7 +312,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action openHeader = (col: any, screenx: number, screeny: number) => { this._col = col; - this._headerOpen = !this._headerOpen; + this._headerOpen = true; this._pointerX = screenx; this._pointerY = screeny; } @@ -392,7 +344,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action onHeaderClick = (e: React.PointerEvent) => { - this.props.active(true); e.stopPropagation(); } @@ -408,7 +359,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { TraceMobx(); return <div className="collectionSchema-header-menuOptions"> {this.renderTypes(this._col)} - {this.renderSorting(this._col)} + {/* {this.renderSorting(this._col)} */} {this.renderColors(this._col)} <div className="collectionSchema-headerMenu-group"> <button onClick={() => { this.deleteColumn(this._col.heading); }} @@ -426,7 +377,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action setFocused = (doc: Doc) => this._focusedTable = doc; - @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; + @action setPreviewDoc = (doc: Opt<Doc>) => { + SelectionManager.SelectSchemaDoc(this, doc); + this.previewDoc = doc; + } //toggles preview side-panel of schema @action @@ -487,6 +441,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} docFilters={this.docFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} @@ -531,6 +486,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { documentKeys={this.documentKeys} headerIsEditing={this._headerIsEditing} openHeader={this.openHeader} + onClick={e => { e.stopPropagation(); this.closeHeader(); }} onPointerDown={this.onTablePointerDown} onResizedChange={this.onResizedChange} setColumns={this.setColumns} @@ -552,15 +508,29 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> </div>; } + onSpecificMenu = (e: React.MouseEvent) => { + if ((e.target as any)?.className?.includes?.("collectionSchemaView-cell") || (e.target instanceof HTMLSpanElement)) { + const cm = ContextMenu.Instance; + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + optionItems.push({ description: "remove", event: () => this.previewDoc && this.props.removeDocument(this.previewDoc), icon: "trash" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + cm.displayMenu(e.clientX, e.clientY); + (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. + e.stopPropagation(); + } + } @action onTablePointerDown = (e: React.PointerEvent): void => { + if (!(e.target as any)?.className?.includes?.("collectionSchemaView-cell") && !(e.target instanceof HTMLSpanElement)) { + this.setPreviewDoc(undefined); + } this.setFocused(this.props.Document); if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { e.stopPropagation(); } - this._pointerY = e.screenY; - this._pointerX = e.screenX; + // this.closeHeader(); } onResizedChange = (newResized: Resize[], event: any) => { @@ -610,17 +580,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } render() { let name = "collectionSchemaView-container"; - if (this.props.Document._searchDoc !== undefined) { + if (this.props.Document._searchDoc) { name = "collectionSchemaView-searchContainer"; } + if (!this.props.active()) setTimeout(() => this.closeHeader(), 0); TraceMobx(); const menuContent = this.renderMenuContent; - const menu = <div className="collectionSchema-header-menu" ref={this.setNode} + const menu = <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ position: "fixed", background: "white", border: "black 1px solid", - transform: `translate(${(this.menuCoordinates[0] / this.scale)}px, ${(this.menuCoordinates[1] / this.scale)}px)` + transform: `translate(${(this.menuCoordinates[0])}px, ${(this.menuCoordinates[1])}px)` }}> <Measure offset onResize={action((r: any) => { const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); @@ -631,13 +602,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div>; return <div className={name} style={{ - overflow: this.props.overflow === true ? "scroll" : undefined, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, + overflow: this.props.overflow === true ? "scroll" : undefined, backgroundColor: "white", + pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, width: name === "collectionSchemaView-searchContainer" ? "auto" : this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", }} > <div className="collectionSchemaView-tableContainer" - style={{ backgroundColor: "white", width: `calc(100% - ${this.previewWidth()}px)` }} + style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onKeyPress={this.onKeyPress} + onContextMenu={this.onSpecificMenu} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} @@ -646,7 +618,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { </div> {this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} - {this._headerOpen ? menu : null} + {this._headerOpen && this.props.active() ? menu : null} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index fe3d57bdb..fb0bce53e 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -38,6 +38,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef<HTMLDivElement>(); _pivotFieldDisposer?: IReactionDisposer; + _autoHeightDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; @observable _heightMap = new Map<string, number>(); @@ -47,7 +48,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) @computed get pivotField() { return StrCast(this.layoutDoc._pivotField); } @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 0)); } // 2 * this.gridGap)); } + @computed get yMargin() { return Math.max(this.layoutDoc._showTitle && !this.layoutDoc._showTitleHover ? 30 : 0, NumCast(this.layoutDoc._yMargin, 5)); } // 2 * this.gridGap)); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return BoolCast(this.layoutDoc._columnsStack, true); } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } @@ -76,6 +77,9 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const dxf = () => this.getDocTransform(d, dref.current!); this._docXfs.push({ dxf, width, height }); const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + if (height() < 5) { + console.log("here" + height()); + } const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)} @@ -148,10 +152,12 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) () => this.pivotField, () => this.layoutDoc._columnHeaders = new List() ); + this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, this.forceAutoHeight); } componentWillUnmount() { super.componentWillUnmount(); this._pivotFieldDisposer?.(); + this._autoHeightDisposer?.(); } @action @@ -193,7 +199,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) { const height = () => this.getDocHeight(doc); - const opacity = () => this.Document.currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document.currentFrame))?.opacity; + const opacity = () => this.Document._currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity; return <ContentFittingDocumentView Document={doc} DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])} @@ -217,6 +223,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) opacity={opacity} focus={this.focusDocument} docFilters={this.docFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} @@ -249,8 +256,10 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); return wid * aspect; } - return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin : - Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]()); + return layoutDoc._fitWidth ? + (!nh ? this.props.PanelHeight() - 2 * this.yMargin : + Math.min(wid * nh / (nw || 1), this.layoutDoc._autoHeight ? 100000 : this.props.PanelHeight() - 2 * this.yMargin)) : + Math.max(20, layoutDoc[HeightSym]()); } columnDividerDown = (e: React.PointerEvent) => { @@ -340,9 +349,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; - const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); - if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - type = types[0]; + if (this.pivotField) { + const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); + if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { + type = types[0]; + } } const cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length, Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); @@ -354,16 +365,16 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { - Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + Doc.Layout(doc)._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); } })); this.observer.observe(ref); } }} - key={heading ? heading.heading : ""} + key={heading?.heading ?? ""} cols={cols} headings={this.headings} - heading={heading ? heading.heading : ""} + heading={heading?.heading ?? ""} headingObject={heading} docList={docList} parent={this} @@ -383,6 +394,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) return this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + offsety); } + forceAutoHeight = () => { + const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; + Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); + } + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[], first: boolean) => { const key = this.pivotField; let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; @@ -449,6 +465,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._columnsFill ? "Variable Size" : "Autosize"} Column`, event: () => this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill, icon: "plus" }); subItems.push({ description: `${this.layoutDoc._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + subItems.push({ description: "Clear All", event: () => this.dataDoc.data = new List([]), icon: "times" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } @@ -489,7 +506,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) transformOrigin: "top left", }} onScroll={action(e => { - if (!this.props.isSelected() && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; + if (!this.props.isSelected(true) && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; else this._scroll = e.currentTarget.scrollTop; })} onDrop={this.onExternalDrop.bind(this)} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index f193a9787..fd2ae03d8 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,35 +1,31 @@ import React = require("react"); -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, runInAction, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec } from "../../../fields/Schema"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; -import { NumCast, StrCast, Cast } from "../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, setupMoveUpEvents } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { EditableView } from "../EditableView"; import { CollectionStackingView } from "./CollectionStackingView"; -import { setupMoveUpEvents, emptyFunction } from "../../../Utils"; import "./CollectionStackingView.scss"; -import { listSpec } from "../../../fields/Schema"; -import { SnappingManager } from "../../util/SnappingManager"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faPalette); - interface CSVFieldColumnProps { cols: () => number; headings: () => object[]; @@ -136,7 +132,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC addDocument = (value: string, shiftDown?: boolean) => { if (!value) return false; const key = StrCast(this.props.parent.props.Document._pivotField); - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true }); + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 200, title: value, _autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; @@ -269,7 +265,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" }); ContextMenu.Instance.setDefaultItem("::", (name: string): void => { Doc.GetProto(this.props.parent.props.Document)[name] = ""; - const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true }); + const created = Docs.Create.TextDocument("", { title: name, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 250, _autoHeight: true }); if (created) { if (this.props.parent.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); @@ -289,9 +285,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const heading = this._heading; const style = this.props.parent; const singleColumn = style.isStackingView; - const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin); + const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin, 5); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); - const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; + const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const headerEditableViewProps = { GetValue: () => evContents, SetValue: this.headingChanged, @@ -310,7 +306,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ - marginTop: NumCast(this.props.parent.props.Document._yMargin), + marginTop: NumCast(this.props.parent.props.Document._yMargin, 5), width: (style.columnWidth) / ((uniqueHeadings.length + ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1) diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 3f2ad47a5..8f0710f4b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; +import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; @@ -111,6 +111,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: return this.props.ignoreFields?.includes("_docFilters") ? [] : [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } + searchFilterDocs = () => { + return [...this.props.searchFilterDocs(), ...DocListCast(this.props.Document._searchFilterDocs)]; + } @computed get childDocs() { let rawdocs: (Doc | Promise<Doc>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; @@ -128,52 +131,43 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - let searchDocs = DocListCast(this.props.Document._searchDocs); - + const docFilters = this.docFilters(); + let searchDocs = this.searchFilterDocs(); + if (this.props.Document.dontRegisterView || (!docFilters.length && !searchDocs.length)) return childDocs; - let docsforFilter: Doc[] = childDocs; - - if (searchDocs !== undefined && searchDocs.length > 0) { - docsforFilter = []; - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - console.log(searchDocs); - searchDocs = DocUtils.FilterDocs(searchDocs, this.docFilters(), docRangeFilters, viewSpecScript); - console.log(this.docFilters()); - console.log(searchDocs); - childDocs.forEach((d) => { - if (d.data !== undefined) { - let newdocs = DocListCast(d.data); - if (newdocs.length > 0) { - let displaycheck: boolean | undefined = undefined; - let newarray: Doc[] = []; - while (newdocs.length > 0) { - newarray = []; - newdocs.forEach((t) => { - if (d.data !== undefined) { - const newdocs = DocListCast(t.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - if (searchDocs.includes(t)) { - displaycheck = true; - } - }); - newdocs = newarray; - } - if (displaycheck === true) { - docsforFilter.push(d); - } + const docsforFilter: Doc[] = []; + const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); + childDocs.forEach((d) => { + if (this.props.Document.title === "lose this") { + console.log('here"') + } + if (d.title === "lose this") { + console.log('here"') + } + let notFiltered = d.z || ((!searchDocs.length || searchDocs.includes(d)) && (!docFilters.length || DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript).length > 0)); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + if (data !== undefined) { + let subDocs = DocListCast(data); + if (subDocs.length > 0) { + let newarray: Doc[] = []; + notFiltered = notFiltered || (!searchDocs.length && docFilters.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript).length); + while (subDocs.length > 0 && !notFiltered) { + newarray = []; + subDocs.forEach((t) => { + const fieldKey = Doc.LayoutFieldKey(t); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && (!docFilters.length || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript).length)); + DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + }); + subDocs = newarray; } } - if (searchDocs.includes(d)) { - docsforFilter.push(d); - } - }); - return docsforFilter; - } - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - return this.props.Document.dontRegisterView ? childDocs : DocUtils.FilterDocs(childDocs, this.docFilters(), docRangeFilters, viewSpecScript); + } + notFiltered && docsforFilter.push(d); + }); + return docsforFilter; } @action @@ -240,13 +234,14 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); } else added = res; - !added && alert("You don't have permission to perform this move"); - e.stopPropagation(); + added && e.stopPropagation(); + return added; } else { ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); } - added && e.stopPropagation(); + !added && alert("You cannot perform this move"); + e.stopPropagation(); return added; } else if (de.complete.annoDragData) { @@ -296,7 +291,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); + this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 100, _height: 25 })); } return; } @@ -408,7 +403,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: _height: 315, _nativeWidth: 850, _nativeHeight: 962, - UseCors: true + useCors: true }); newDoc.data = new WebField(uriList.split("#annotations:")[0]); // clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) this.addDocument(newDoc); @@ -472,7 +467,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: completed?.(); } else { if (text && !text.includes("https://")) { - UndoManager.RunInBatch(() => this.addDocument(Docs.Create.TextDocument(text, { ...options, title: text.substring(0, 20), _width: 400, _height: 315 })), "drop"); + UndoManager.RunInBatch(() => this.addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, title: text.substring(0, 20), _width: 400, _height: 315 })), "drop"); } } disposer(); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index c9bf82406..f96a5c4f0 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -85,6 +85,15 @@ white-space: pre-wrap; min-width: 10px; } +.docContainer-system { + font-variant: all-small-caps; + border-radius: 5px; + background: grey; + color: white; + padding-left: 3px; + padding-right: 3px; + padding-bottom: 2px; +} .treeViewItem-openRight { display: none; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d096e7d66..f13fee776 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,17 +1,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym, DocListCastOrNull } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { PrefetchProxy } from '../../../fields/Proxy'; import { Document, listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter } from '../../../Utils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, simulateMouseClick, Utils, returnEmptyFilter, returnEmptyDoclist } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; -import { DocumentManager } from '../../util/DocumentManager'; import { SnappingManager } from '../../util/SnappingManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; @@ -21,7 +20,6 @@ import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from "../EditableView"; -import { MainView } from '../MainView'; import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView'; import { DocumentView } from '../nodes/DocumentView'; import { ImageBox } from '../nodes/ImageBox'; @@ -33,15 +31,16 @@ import { CollectionViewType } from './CollectionView'; import React = require("react"); import { makeTemplate } from '../../util/DropConverter'; import { TraceMobx } from '../../../fields/util'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { CollectionDockingView } from './CollectionDockingView'; export interface TreeViewProps { document: Doc; dataDoc?: Doc; - libraryPath: Doc[] | undefined; containingCollection: Doc; prevSibling?: Doc; renderDepth: number; - deleteDoc: (doc: Doc | Doc[]) => boolean; + removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; moveDocument: DragManager.MoveFunction; dropAction: dropActionType; addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean; @@ -55,7 +54,7 @@ export interface TreeViewProps { ScreenToLocalTransform: () => Transform; backgroundColor?: (doc: Doc, renderDepth: number) => string | undefined; outerXf: () => { translateX: number, translateY: number }; - treeViewDoc: Doc; + treeView: CollectionTreeView; parentKey: string; active: (outsideReaction?: boolean) => boolean; treeViewHideHeaderFields: () => boolean; @@ -64,6 +63,8 @@ export interface TreeViewProps { onCheckedClick?: () => ScriptField; onChildClick?: () => ScriptField; ignoreFields?: string[]; + firstLevel: boolean; + whenActiveChanged: (isActive: boolean) => void; } @observer @@ -77,6 +78,7 @@ export interface TreeViewProps { */ class TreeView extends React.Component<TreeViewProps> { private _editTitleScript: (() => ScriptField) | undefined; + private _openScript: (() => ScriptField) | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); @@ -88,23 +90,25 @@ class TreeView extends React.Component<TreeViewProps> { get doc() { return this.props.document; } get noviceMode() { return BoolCast(Doc.UserDoc().noviceMode, false); } get displayName() { return "TreeView(" + this.doc.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs.length ? this.fieldKey : StrCast(this.doc.defaultExpandedView, this.noviceMode ? "layout" : "fields"); } + get treeViewLockExpandedView() { return this.doc.treeViewLockExpandedView; } + get defaultExpandedView() { return StrCast(this.doc.treeViewDefaultExpandedView, this.noviceMode ? "layout" : "fields"); } + get treeViewDefaultExpandedView() { return this.treeViewLockExpandedView ? this.defaultExpandedView : (this.childDocs ? this.fieldKey : this.defaultExpandedView); } @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.doc.treeViewOpen = this._overrideTreeViewOpen = c; } @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.doc.treeViewPreventOpen && BoolCast(this.doc.treeViewOpen)) || this._overrideTreeViewOpen; } - @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.defaultExpandedView); } + @computed get treeViewExpandedView() { return StrCast(this.doc.treeViewExpandedView, this.treeViewDefaultExpandedView); } @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } @computed get dataDoc() { return this.doc[DataSym]; } @computed get layoutDoc() { return Doc.Layout(this.doc); } @computed get fieldKey() { const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } childDocList(field: string) { const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; - return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? DocListCast(layout[field]) : undefined) || // else if there's a layout doc, display it's fields - DocListCast(this.doc[field])); // otherwise use the document's data field + return ((this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field + (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields + DocListCastOrNull(this.doc[field])); // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } @@ -114,22 +118,25 @@ class TreeView extends React.Component<TreeViewProps> { Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } - @undoBatch openRight = () => this.props.addDocTab(this.doc, "onRight", this.props.libraryPath); + @undoBatch openRight = () => this.props.addDocTab(this.doc, "add:right"); @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.doc !== target && this.props.deleteDoc(doc) && addDoc(doc); + return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc); } @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); } @undoBatch @action removeDoc = (doc: Doc | Doc[]) => { return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.RemoveDocFromList(this.props.containingCollection, Doc.LayoutFieldKey(this.props.containingCollection), doc), true); + flg && Doc.RemoveDocFromList(this.doc, Doc.LayoutFieldKey(this.doc), doc), true); } constructor(props: any) { super(props); - const script = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); - this._editTitleScript = script && (() => script); + const titleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); documentView.select();} `, { documentView: "any" }); + const openScript = ScriptField.MakeScript(`openOnRight(self)`); + const treeOpenScript = ScriptField.MakeScript(`self.treeViewOpen = !self.treeViewOpen`); + this._editTitleScript = !Doc.IsSystem(this.props.document) ? titleScript && (() => titleScript) : treeOpenScript && (() => treeOpenScript); + this._openScript = !Doc.IsSystem(this.props.document) ? openScript && (() => openScript) : undefined; if (Doc.GetT(this.doc, "editTitle", "string", true) === "*") Doc.SetInPlace(this.doc, "editTitle", this._uniqueId, false); } @@ -193,13 +200,13 @@ class TreeView extends React.Component<TreeViewProps> { }} OnTab={undoBatch((shift?: boolean) => { shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); - setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", "*", false), 0); + setTimeout(() => Doc.SetInPlace(this.doc, "editTitle", `${this.props.treeView._uniqueId}`, false), 0); })} />) preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; - dragData && (dragData.dropAction = this.props.treeViewDoc === dragData.treeViewDoc ? "same" : dragData.dropAction); + dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? "same" : dragData.dropAction); } @undoBatch @@ -274,11 +281,11 @@ class TreeView extends React.Component<TreeViewProps> { const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce( (flg, doc) => flg && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true), true); - contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : - DocListCast(contents), this.props.treeViewDoc, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), + this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields); + [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged); } else { contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} @@ -320,16 +327,16 @@ class TreeView extends React.Component<TreeViewProps> { (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true), true); const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; const sortKey = `${this.fieldKey}-sortAscending`; - return <ul key={expandKey + "more"} onClick={(e) => { + return <ul key={expandKey + "more"} className={this.doc.treeViewHideTitle ? "no-indent" : ""} onClick={(e) => { this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true)); e.stopPropagation(); }}> {!docs ? (null) : - TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc, + TreeView.GetChildElements(docs, this.props.treeView, this.layoutDoc, this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, - [...this.props.renderedIds, this.doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} + [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields, false, this.props.whenActiveChanged)} </ul >; } else if (this.treeViewExpandedView === "fields") { return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > @@ -357,6 +364,7 @@ class TreeView extends React.Component<TreeViewProps> { focus={returnFalse} ScreenToLocalTransform={this.docTransform} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.containingCollection} ContainingCollectionView={undefined} addDocument={returnFalse} @@ -383,7 +391,7 @@ class TreeView extends React.Component<TreeViewProps> { this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, heading: this.props.containingCollection.title, checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", - containingTreeView: this.props.treeViewDoc, + containingTreeView: this.props.treeView.props.Document, }, console.log); } else { this.treeViewOpen = !this.treeViewOpen; @@ -405,11 +413,13 @@ class TreeView extends React.Component<TreeViewProps> { showContextMenu = (e: React.MouseEvent) => { this._docRef.current?.ContentDiv && simulateMouseClick(this._docRef.current.ContentDiv, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30); } - focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); - contextMenuItems = () => [{ script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; - truncateTitleWidth = () => NumCast(this.props.treeViewDoc.treeViewTruncateTitleWidth, 0); - showTitleEdit = () => ["*", this._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); - onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.editTitleScript)); + contextMenuItems = () => Doc.IsSystem(this.doc) ? [] : [{ script: ScriptField.MakeFunction(`openOnRight(self)`)!, label: "Open" }, { script: ScriptField.MakeFunction(`DocFocus(self)`)!, label: "Focus" }]; + truncateTitleWidth = () => NumCast(this.props.treeView.props.Document.treeViewTruncateTitleWidth, 0); + @computed get showTitleEdit() { + return ["*", this._uniqueId, this.props.treeView._uniqueId].includes(Doc.GetT(this.doc, "editTitle", "string", true) || ""); + } + onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); + onChildDoubleClick = () => (!this.props.treeView.props.Document.treeViewOutlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick); /** * Renders the EditableView title element for placement into the tree. */ @@ -418,36 +428,38 @@ class TreeView extends React.Component<TreeViewProps> { TraceMobx(); const headerElements = this.props.treeViewHideHeaderFields() ? (null) : <> - <FontAwesomeIcon icon="cog" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> + <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} /> <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { if (this.treeViewOpen) { - this.doc.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : - this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : - this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : - (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : - this.childDocs.length ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); + this.doc.treeViewExpandedView = this.treeViewLockExpandedView ? this.doc.treeViewExpandedView : + this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode ? "layout" : "fields") : + this.treeViewExpandedView === "fields" && this.layoutDoc ? "layout" : + this.treeViewExpandedView === "layout" && DocListCast(this.doc.links).length ? "links" : + (this.treeViewExpandedView === "links" || this.treeViewExpandedView === "layout") && DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : + this.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); } this.treeViewOpen = true; })}> {this.treeViewExpandedView} </span> </>; - const view = this.showTitleEdit() ? this.editableView("title") : + const view = this.showTitleEdit ? this.editableView("title") : <DocumentView ref={this._docRef} Document={this.doc} DataDoc={undefined} - treeViewDoc={this.props.treeViewDoc} - LibraryPath={this.props.libraryPath || emptyPath} + treeViewDoc={this.props.treeView.props.Document} + LibraryPath={emptyPath} addDocument={undefined} addDocTab={this.props.addDocTab} rootSelected={returnTrue} pinToPres={emptyFunction} onClick={this.onChildClick} + onDoubleClick={this.onChildDoubleClick} dropAction={this.props.dropAction} moveDocument={this.move} - removeDocument={this.removeDoc} + removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} ContentScaling={returnOne} PanelWidth={this.truncateTitleWidth} @@ -459,34 +471,32 @@ class TreeView extends React.Component<TreeViewProps> { renderDepth={1} focus={returnTrue} parentActive={returnTrue} - whenActiveChanged={emptyFunction} + whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} - dontRegisterView={BoolCast(this.props.treeViewDoc.dontRegisterChildViews)} + dontRegisterView={BoolCast(this.props.treeView.props.Document.dontRegisterChildViews)} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.containingCollection} />; return <> - <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} + <div className={`docContainer${Doc.IsSystem(this.props.document) ? "-system" : ""}`} ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} style={{ - fontWeight: this.doc.searchMatch ? "bold" : undefined, + fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? "bold" : undefined, textDecoration: Doc.GetT(this.doc, "title", "string", true) ? "underline" : undefined, - outline: BoolCast(this.doc.workspaceBrush) ? "dashed 1px #06123232" : undefined, - pointerEvents: this.props.active() || SnappingManager.GetIsDragging() ? undefined : "none" + outline: this.doc === CurrentUserUtils.ActiveDashboard ? "dashed 1px #06123232" : undefined, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} > {view} </div > - {headerElements} - <div className="treeViewItem-openRight" onClick={this.openRight}> - <FontAwesomeIcon title="open in a new pane" icon="external-link-alt" size="sm" /> - </div> + {Doc.IsSystem(this.doc) && Doc.UserDoc().noviceMode ? (null) : headerElements} </>; } render() { TraceMobx(); const sorting = this.doc[`${this.fieldKey}-sortAscending`]; - if (this.showTitleEdit()) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll + if (this.showTitleEdit) { // find containing CollectionTreeView and set our maximum width so the containing tree view won't have to scroll let par: any = this._header?.current; if (par) { while (par && par.className !== "collectionTreeView-dropTarget") par = par.parentNode; @@ -497,34 +507,35 @@ class TreeView extends React.Component<TreeViewProps> { } } } else this._editMaxWidth = ""; - return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> - <li className="collection-child"> - <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { - if (this.props.active(true)) { - e.stopPropagation(); - e.preventDefault(); - SelectionManager.DeselectAll(); - } - }} - onPointerDown={e => { + return this.doc.treeViewHideTitle && this.props.firstLevel ? this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent : + <div className="treeViewItem-container" ref={this.createTreeDropTarget} onPointerDown={e => this.props.active(true) && SelectionManager.DeselectAll()}> + <li className="collection-child"> + <div className={`treeViewItem-header` + (this._editMaxWidth ? "-editing" : "")} ref={this._header} style={{ maxWidth: this._editMaxWidth }} onClick={e => { if (this.props.active(true)) { e.stopPropagation(); e.preventDefault(); + SelectionManager.DeselectAll(); } }} - onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - {this.renderBullet} - {this.renderTitle} - </div> - <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} - </div> - </li> - </div>; + onPointerDown={e => { + if (this.props.active(true)) { + e.stopPropagation(); + e.preventDefault(); + } + }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> + {this.renderBullet} + {this.renderTitle} + </div> + <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> + {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} + </div> + </li> + </div>; } public static GetChildElements( childDocs: Doc[], - treeViewDoc: Doc, + treeView: CollectionTreeView, containingCollection: Doc, dataDoc: Doc | undefined, key: string, @@ -546,10 +557,11 @@ class TreeView extends React.Component<TreeViewProps> { treeViewHideHeaderFields: () => boolean, treeViewPreventOpen: boolean, renderedIds: string[], - libraryPath: Doc[] | undefined, onCheckedClick: undefined | (() => ScriptField), onChildClick: undefined | (() => ScriptField), - ignoreFields: string[] | undefined + ignoreFields: string[] | undefined, + firstLevel: boolean, + whenActiveChanged: (isActive: boolean) => void ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -601,7 +613,7 @@ class TreeView extends React.Component<TreeViewProps> { } const indent = i === 0 ? undefined : () => { - if (StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { + if (remove && StrCast(docs[i - 1].layout).indexOf('fieldKey') !== -1) { const fieldKeysub = StrCast(docs[i - 1].layout).split('fieldKey')[1]; const fieldKey = fieldKeysub.split("\'")[1]; if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) { @@ -612,7 +624,7 @@ class TreeView extends React.Component<TreeViewProps> { } }; const outdent = !parentCollectionDoc ? undefined : () => { - if (StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { + if (remove && StrCast(parentCollectionDoc.layout).indexOf('fieldKey') !== -1) { const fieldKeysub = StrCast(parentCollectionDoc.layout).split('fieldKey')[1]; const fieldKey = fieldKeysub.split("\'")[1]; Doc.AddDocToList(parentCollectionDoc, fieldKey, child, parentPrevSibling, false); @@ -631,17 +643,16 @@ class TreeView extends React.Component<TreeViewProps> { return !(child instanceof Doc) ? (null) : <TreeView document={pair.layout} dataDoc={pair.data} - libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined} containingCollection={containingCollection} prevSibling={docs[i]} - treeViewDoc={treeViewDoc} + treeView={treeView} key={child[Id]} indentDocument={indent} outdentDocument={outdent} onCheckedClick={onCheckedClick} onChildClick={onChildClick} renderDepth={renderDepth} - deleteDoc={remove} + removeDoc={StrCast(containingCollection.freezeChildren).includes("remove") ? undefined : remove} addDocument={addDocument} backgroundColor={backgroundColor} panelWidth={rowWidth} @@ -658,7 +669,9 @@ class TreeView extends React.Component<TreeViewProps> { treeViewHideHeaderFields={treeViewHideHeaderFields} treeViewPreventOpen={treeViewPreventOpen} renderedIds={renderedIds} - ignoreFields={ignoreFields} />; + ignoreFields={ignoreFields} + firstLevel={firstLevel} + whenActiveChanged={whenActiveChanged} />; }); } } @@ -675,6 +688,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; + public _uniqueId = Utils.GenerateGuid(); @computed get doc() { return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } @@ -725,14 +739,14 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myWorkspaces) { - ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); - ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.doc), icon: "minus" }); + if (!e.isPropagationStopped() && this.doc === CurrentUserUtils.MyDashboards) { + ContextMenu.Instance.addItem({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "plus" }); + ContextMenu.Instance.addItem({ description: "Delete Dashboard", event: () => this.remove(this.doc), icon: "minus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); - } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosed) { - ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosed = new List<Doc>(), icon: "plus" }); + } else if (!e.isPropagationStopped() && this.doc === Doc.UserDoc().myRecentlyClosedDocs) { + ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.UserDoc().myRecentlyClosedDocs = new List<Doc>(), icon: "plus" }); e.stopPropagation(); e.preventDefault(); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); @@ -788,6 +802,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); + onClicks.push({ + description: `${this.props.Document.treeViewOutlineMode ? "Delay " : "Immediate "} Title Editing`, event: () => UndoManager.RunInBatch(() => this.props.Document.treeViewOutlineMode = !this.props.Document.treeViewOutlineMode, "edit onCheckedClick"), icon: "edit" + }); !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); @@ -805,6 +822,11 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onChildClick = () => { return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); } + _isChildActive = false; + whenActiveChanged = (isActive: boolean) => { + this.props.whenActiveChanged(this._isChildActive = isActive); + } + active = (outsideReaction: boolean | undefined) => this.props.active(outsideReaction) || this._isChildActive; render() { TraceMobx(); if (!(this.doc instanceof Doc)) return (null); @@ -812,6 +834,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(d, target, addDoc); const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs; + const childElements = childDocs && TreeView.GetChildElements(childDocs, this, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + this.outerXf, this.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), + BoolCast(this.doc.treeViewPreventOpen), [], this.props.onCheckedClick, + this.onChildClick, this.props.ignoreFields, true, this.whenActiveChanged); + const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; + return !childDocs ? (null) : ( <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> <div className="collectionTreeView-dropTarget" id="body" @@ -820,12 +849,12 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll paddingLeft: `${NumCast(this.doc._xPadding, 10)}px`, paddingRight: `${NumCast(this.doc._xPadding, 10)}px`, paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, - pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() && !this._isChildActive ? "none" : undefined, }} onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> - {this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : <EditableView + {hideTitle ? (null) : <EditableView contents={this.dataDoc.title} editing={false} display={"block"} @@ -840,42 +869,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); })} />} {this.doc.allowClear ? this.renderClearButton : (null)} - <ul className="no-indent" style={{ width: "max-content" }} > - { - TreeView.GetChildElements(childDocs, this.doc, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), - BoolCast(this.doc.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.onChildClick, this.props.ignoreFields) - } - </ul> + <ul className="no-indent" style={{ width: "max-content" }} > {childElements} </ul> </div > </div> ); } } -Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) { - const allCollectionDocs = DocListCast(dataDoc[dataKey]); - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => - set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); - - let nonNumbers = 0; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } - }); - const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { - const doc = new Doc(); - doc.title = facetValue.toString(); - doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); - return doc; - }); - return new List<Doc>(facetValueDocSet); -}); - Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) { const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []); for (let i = 0; i < docFilters.length; i += 3) { diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 6dd21ef7f..ba8e23447 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,6 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faEye } from '@fortawesome/free-regular-svg-icons'; -import { faColumns, faCopy, faEllipsisV, faFingerprint, faGlobeAmericas, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable } from 'mobx'; import { observer } from "mobx-react"; @@ -18,7 +15,7 @@ import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util'; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnEmptyDoclist } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -53,8 +50,6 @@ export const Flyout = higflyout.default; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); -library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faGlobeAmericas, faEllipsisV, faImage, faEye as any, faCopy); - export enum CollectionViewType { Invalid = "invalid", Freeform = "freeform", @@ -69,7 +64,7 @@ export enum CollectionViewType { Carousel = "carousel", Carousel3D = "3D Carousel", Linear = "linear", - Staff = "staff", + //Staff = "staff", Map = "map", Grid = "grid", Pile = "pileup" @@ -139,6 +134,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } const docs = doc instanceof Doc ? [doc] : doc; + + + if (docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document))) return false; const targetDataDoc = this.props.Document[DataSym]; const docList = DocListCast(targetDataDoc[this.props.fieldKey]); const added = docs.filter(d => !docList.includes(d)); @@ -159,7 +157,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } if (effectiveAcl === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); + added.map(doc => { + Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc); + doc.context = this.props.Document; + }); } else { added.map(doc => { @@ -177,14 +178,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); doc.displayTimecode = undefined; } + doc._stayInCollection = undefined; doc.context = this.props.Document; }); - added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add)); - // targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]); (targetDataDoc[this.props.fieldKey] as List<Doc>).push(...added); targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); - const lastModified = "lastModified"; - targetDataDoc[lastModified] = new DateField(new Date(Date.now())); } } } @@ -200,11 +198,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const value = DocListCast(targetDataDoc[this.props.fieldKey]); const toRemove = value.filter(v => docs.includes(v)); if (toRemove.length !== 0) { - const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc); recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); - doc.deleted = true; }); return true; } @@ -253,7 +250,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />); - case CollectionViewType.Staff: return (<CollectionStaffView key="collview" {...props} />); + //case CollectionViewType.Staff: return (<CollectionStaffView key="collview" {...props} />); case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView key="collview" {...props} />); case CollectionViewType.Multirow: return (<CollectionMultirowView key="rpwview" {...props} />); case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } @@ -274,7 +271,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return this.SubViewHelper(type, renderProps); } - setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { const subItems: ContextMenuProps[] = []; @@ -286,7 +282,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus subItems.push({ description: "Tree", event: () => func(CollectionViewType.Tree), icon: "tree" }); subItems.push({ description: "Stacking", event: () => func(CollectionViewType.Stacking), icon: "ellipsis-v" }); subItems.push({ description: "Stacking (AutoHeight)", event: () => func(CollectionViewType.Stacking)._autoHeight = true, icon: "ellipsis-v" }); - subItems.push({ description: "Staff", event: () => func(CollectionViewType.Staff), icon: "music" }); subItems.push({ description: "Multicolumn", event: () => func(CollectionViewType.Multicolumn), icon: "columns" }); subItems.push({ description: "Multirow", event: () => func(CollectionViewType.Multirow), icon: "columns" }); subItems.push({ description: "Masonry", event: () => func(CollectionViewType.Masonry), icon: "columns" }); @@ -309,7 +304,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.props.Document); newRendition._viewType = vtype; - this.props.addDocTab(newRendition, "onRight"); + this.props.addDocTab(newRendition, "add:right"); return newRendition; }, false); @@ -317,10 +312,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const optionItems = options && "subitems" in options ? options.subitems : []; !Doc.UserDoc().noviceMode ? optionItems.splice(0, 0, { description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }) : null; if (this.props.Document.childLayout instanceof Doc) { - optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "add:right"), icon: "project-diagram" }); } if (this.props.Document.childClickedOpenTemplateView instanceof Doc) { - optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" }); + optionItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "add:right"), icon: "project-diagram" }); } !Doc.UserDoc().noviceMode && optionItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); @@ -335,7 +330,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => { const alias = Doc.MakeAlias(this.props.Document); DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, "onRight"); + this.props.addDocTab(alias, "add:right"); } })); DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick => @@ -371,6 +366,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)} onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />); } + get _facetWidth() { return NumCast(this.props.Document._facetWidth); } set _facetWidth(value) { this.props.Document._facetWidth = value; } @@ -394,9 +390,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } + get childDocList() { return Cast(this.dataField, listSpec(Doc)); } + get childDocs() { const dfield = this.dataField; const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []); @@ -404,91 +402,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus const viewSpecScript = ScriptCast(this.props.Document.viewSpecScript); return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } - @computed get _allFacets() { - TraceMobx(); - const facets = new Set<string>(["type", "text", "data", "author", "ACL"]); - this.childDocs.filter(child => child).forEach(child => child && Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); - Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.filter(child => child).forEach(child => Object.keys(child).forEach(key => facets.add(key))); - return Array.from(facets).filter(f => !f.startsWith("_") && !["proto", "zIndex", "isPrototype", "context", "text-noTemplate"].includes(f)).sort(); - } - - /** - * Responds to clicking the check box in the flyout menu - */ - facetClick = (facetHeader: string) => { - const facetCollection = this.props.Document; - const found = DocListCast(facetCollection[this.props.fieldKey + "-filter"]).findIndex(doc => doc.title === facetHeader); - if (found !== -1) { - (facetCollection[this.props.fieldKey + "-filter"] as List<Doc>).splice(found, 1); - const docFilter = Cast(this.props.Document._docFilters, listSpec("string")); - if (docFilter) { - let index: number; - while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { - docFilter.splice(index, 3); - } - } - const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string")); - if (docRangeFilters) { - let index: number; - while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { - docRangeFilters.splice(index, 3); - } - } - } else { - const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - var rtfields = 0; - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => { - const field = child[facetHeader] as Field; - const fieldStr = Field.toString(field); - if (field instanceof RichTextField || (typeof (field) === "string" && fieldStr.split(" ").length > 2)) rtfields++; - return set.add(fieldStr); - }, new Set<string>())); - - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; - facetValues.map(val => { - const num = Number(val); - if (Number.isNaN(num)) { - nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); - } - }); - let newFacet: Opt<Doc>; - if (facetHeader === "text" || rtfields / allCollectionDocs.length > 0.1) { - newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet.target = this.props.Document; - newFacet._textBoxPadding = 4; - const scriptText = `setDocFilter(this.target, "${facetHeader}", text, "match")`; - newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); - } else if (nonNumbers / facetValues.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader, treeViewExpandedView: "layout", treeViewOpen: true }); - const newFacetField = Doc.LayoutFieldKey(newFacet); - const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader); - Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); - const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); - newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; - newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; - Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; - Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; - newFacet.target = this.props.Document; - const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`; - newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); - Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } else { - newFacet = new Doc(); - newFacet.title = facetHeader; - newFacet.treeViewOpen = true; - newFacet.type = DocumentType.COL; - const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc }; - newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, {}, capturedVariables); - } - newFacet && Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet); - } - } onPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { @@ -497,77 +410,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0), false); } - filterBackground = () => "rgba(105, 105, 105, 0.432)"; - get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) - @computed get scriptField() { - const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)"; - const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); - return script ? () => script : undefined; - } - @computed get filterView() { - TraceMobx(); - const facetCollection = this.props.Document; - const flyout = ( - <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> - {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}> - <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} /> - <span className="checkmark" /> - {facet} - </label>)} - </div> - ); - - return !this._facetWidth || this.props.dontRegisterView ? (null) : <div className="collectionTimeView-treeView" style={{ width: `${this.facetWidth()}px`, overflow: this.facetWidth() < 15 ? "hidden" : undefined }}> - <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <div className="collectionTimeView-button"> - <FontAwesomeIcon icon={faEdit} size={"lg"} /> - <span className="collectionTimeView-span">Facet Filters</span> - </div> - </Flyout> - </div> - <div className="collectionTimeView-tree" key="tree"> - <CollectionTreeView - PanelPosition={""} - Document={facetCollection} - DataDoc={facetCollection} - fieldKey={`${this.props.fieldKey}-filter`} - CollectionView={this} - docFilters={returnEmptyFilter} - ContainingCollectionDoc={this.props.ContainingCollectionDoc} - ContainingCollectionView={this.props.ContainingCollectionView} - PanelWidth={this.facetWidth} - PanelHeight={this.props.PanelHeight} - NativeHeight={returnZero} - NativeWidth={returnZero} - LibraryPath={emptyPath} - rootSelected={this.props.rootSelected} - renderDepth={1} - dropAction={this.props.dropAction} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} - addDocTab={returnFalse} - pinToPres={returnFalse} - isSelected={returnFalse} - select={returnFalse} - bringToFront={emptyFunction} - active={this.props.active} - whenActiveChanged={returnFalse} - treeViewHideTitle={true} - ContentScaling={returnOne} - focus={returnFalse} - treeViewHideHeaderFields={true} - onCheckedClick={this.scriptField} - ignoreFields={this.ignoreFields} - annotationsKey={""} - dontRegisterView={true} - backgroundColor={this.filterBackground} - moveDocument={returnFalse} - removeDocument={returnFalse} - addDocument={returnFalse} /> - </div> - </div>; - } - childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); @@ -584,10 +426,10 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; - const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : - `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; + const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document._isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : + `${CurrentUserUtils.ActiveDashboard?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; return (<div className={"collectionView"} onContextMenu={this.onContextMenu} - style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> + style={{ pointerEvents: this.props.Document._isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} <div className="collectionView-facetCont" style={{ display: this.props.PanelPosition === "absolute" ? "flex" : "", justifyContent: this.props.PanelPosition === "absolute" ? "center" : "", width: `calc(100% - ${this.facetWidth()}px)` }}> {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} @@ -598,11 +440,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} - {(Doc.UserDoc()?.noviceMode || !this.props.isSelected() && !this.props.Document.forceActive) || this.props.Document.hideFilterView ? (null) : - <div className="collectionView-filterDragger" title="library View Dragger" onPointerDown={this.onPointerDown} - style={{ right: this.facetWidth() - 1, top: this.props.Document._viewType === CollectionViewType.Docking ? "25%" : "60%" }} /> - } - {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView} </div>); } } diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx deleted file mode 100644 index 149d4927b..000000000 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from "react"; -import './ParentDocumentSelector.scss'; -import { Doc } from "../../../fields/Doc"; -import { observer } from "mobx-react"; -import { observable, action, runInAction, trace, computed, reaction, IReactionDisposer } from "mobx"; -import { Id } from "../../../fields/FieldSymbols"; -import { SearchUtil } from "../../util/SearchUtil"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { NumCast, StrCast } from "../../../fields/Types"; -import { CollectionViewType } from "./CollectionView"; -import { DocumentButtonBar } from "../DocumentButtonBar"; -import { DocumentManager } from "../../util/DocumentManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons"; -import { library } from "@fortawesome/fontawesome-svg-core"; -import { DocumentView } from "../nodes/DocumentView"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Tooltip } from "@material-ui/core"; -const higflyout = require("@hig/flyout"); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - -library.add(faCog); - -type SelectorProps = { - Document: Doc, - Stack?: any, - addDocTab(doc: Doc, location: string): void -}; - -@observer -export class SelectorContextMenu extends React.Component<SelectorProps> { - @observable private _docs: { col: Doc, target: Doc }[] = []; - @observable private _otherDocs: { col: Doc, target: Doc }[] = []; - _reaction: IReactionDisposer | undefined; - - componentDidMount() { - this._reaction = reaction(() => this.props.Document, () => this.fetchDocuments(), { fireImmediately: true }); - } - componentWillUnmount() { - this._reaction?.(); - } - async fetchDocuments() { - const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)); - const containerProtoSets = await Promise.all(aliases.map(async alias => - ((await SearchUtil.Search("", true, { fq: `data_l:"${alias[Id]}"` })).docs))); - const containerProtos = containerProtoSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const containerSets = await Promise.all(Array.from(containerProtos.keys()).map(async container => { - return (SearchUtil.GetAliasesOfDocument(container)); - })); - const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = await Promise.all(Array.from(containers.keys()).map(async (dp) => { - return (SearchUtil.GetAliasesOfDocument(dp)); - })); - const doclayouts = Array.from(doclayoutSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()).keys()); - runInAction(() => { - this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); - this._otherDocs = []; - }); - } - - getOnClick({ col, target }: { col: Doc, target: Doc }) { - return () => { - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; - if (col._viewType === CollectionViewType.Freeform) { - const newPanX = NumCast(target.x) + NumCast(target._width) / 2; - const newPanY = NumCast(target.y) + NumCast(target._height) / 2; - col._panX = newPanX; - col._panY = newPanY; - } - this.props.addDocTab(col, "inTab"); // bcz: dataDoc? - }; - } - - render() { - return <div > - <p key="contexts">Contexts:</p> - {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} - {this._otherDocs.length ? <hr key="hr" /> : null} - {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)} - </div>; - } -} - -@observer -export class ParentDocSelector extends React.Component<SelectorProps> { - render() { - const flyout = ( - <div className="parentDocumentSelector-flyout" title=" "> - <SelectorContextMenu {...this.props} /> - </div> - ); - return <div title="Show Contexts" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> - <span className="parentDocumentSelector-button" > - <FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} /> - </span> - </Flyout> - </div>; - } -} - -@observer -export class DockingViewButtonSelector extends React.Component<{ views: () => DocumentView[], Stack: any }> { - customStylesheet(styles: any) { - return { - ...styles, - panel: { - ...styles.panel, - minWidth: "100px" - }, - }; - } - _ref = React.createRef<HTMLDivElement>(); - - @computed get flyout() { - return ( - <div className="ParentDocumentSelector-flyout" title=" " ref={this._ref}> - <DocumentButtonBar views={this.props.views} stack={this.props.Stack} /> - </div> - ); - } - - render() { - return <Tooltip title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom"> - <span onPointerDown={e => { - if (getComputedStyle(this._ref.current!).width !== "100%") { - e.stopPropagation(); e.preventDefault(); - } - this.props.views()[0]?.select(false); - }} className="buttonSelector"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> - <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> - </Flyout> - </span> - </Tooltip>; - } -} diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index a2c529a21..1fb7aa04a 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -5,16 +5,15 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; -import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { Doc, DocListCast, Field, Opt, AclPrivate, AclReadonly, DataSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, returnEmptyDoclist } from "../../../Utils"; import { Docs, DocumentOptions } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -22,11 +21,15 @@ import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; +import { CellProps, CollectionSchemaButtons, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDateCell, CollectionSchemaDocCell, CollectionSchemaImageCell, CollectionSchemaListCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell } from "./CollectionSchemaCells"; import { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import "./CollectionSchemaView.scss"; import { CollectionView } from "./CollectionView"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { GetEffectiveAcl } from "../../../fields/util"; +import { DateField } from "../../../fields/DateField"; +import { ImageField } from "../../../fields/URLField"; enum ColumnType { @@ -45,7 +48,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] + ["_curPage", ColumnType.Number], ["_currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); export interface SchemaTableProps { @@ -70,11 +73,12 @@ export interface SchemaTableProps { isSelected: (outsideReaction?: boolean) => boolean; isFocused: (document: Doc, outsideReaction: boolean) => boolean; setFocused: (document: Doc) => void; - setPreviewDoc: (document: Doc) => void; + setPreviewDoc: (document: Opt<Doc>) => void; columns: SchemaHeaderField[]; documentKeys: any[]; headerIsEditing: boolean; openHeader: (column: any, screenx: number, screeny: number) => void; + onClick: (e: React.MouseEvent) => void; onPointerDown: (e: React.PointerEvent) => void; onResizedChange: (newResized: Resize[], event: any) => void; setColumns: (columns: SchemaHeaderField[]) => void; @@ -90,7 +94,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @observable _cellIsEditing: boolean = false; @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _openCollections: Array<string> = []; + @observable _openCollections: Set<number> = new Set; @observable _showDoc: Doc | undefined; @observable _showDataDoc: any = ""; @@ -135,16 +139,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeSorting = (col: any) => { - if (col.desc === undefined) { - // no sorting - this.props.changeColumnSort(col, true); - } else if (col.desc === true) { - // descending sort - this.props.changeColumnSort(col, false); - } else if (col.desc === false) { - // ascending sort - this.props.changeColumnSort(col, undefined); - } + this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true); } @action @@ -152,7 +147,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { - const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); const columns: Column<Doc>[] = []; const tableIsFocused = this.props.isFocused(this.props.Document, false); @@ -160,26 +154,16 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const focusedCol = this._focusedCell.col; const isEditable = !this.props.headerIsEditing; - if (this.childDocs.reduce((found, doc) => found || doc.type === DocumentType.COL, false)) { - columns.push( - { - expander: true, - Header: "", - width: 30, - Expander: (rowInfo) => { - if (rowInfo.original.type === "collection") { - if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>; - if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>; - } else { - return null; - } - } - } - ); - } - this.props.active; - - const cols = this.props.columns.map(col => { + columns.push({ + expander: true, Header: "", width: 58, + Expander: (rowInfo) => { + return rowInfo.original.type !== DocumentType.COL ? (null) : + <div className="collectionSchemaView-expander" onClick={action(() => (this._openCollections[rowInfo.isExpanded ? "delete" : "add"])(rowInfo.viewIndex))}> + <FontAwesomeIcon icon={rowInfo.isExpanded ? "caret-down" : "caret-right"} size="lg" /> + </div>; + } + }); + columns.push(...this.props.columns.map(col => { const icon: IconProp = this.getColumnType(col) === ColumnType.Number ? "hashtag" : this.getColumnType(col) === ColumnType.String ? "font" : this.getColumnType(col) === ColumnType.Boolean ? "check-square" : this.getColumnType(col) === ColumnType.Doc ? "file" : this.getColumnType(col) === ColumnType.Image ? "image" : this.getColumnType(col) === ColumnType.List ? "list-ul" : @@ -207,29 +191,17 @@ export class SchemaTable extends React.Component<SchemaTableProps> { width={"100%"} />; - - const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; - - const header = - <div //className="collectionSchemaView-header" - //onClick={e => this.props.openHeader(col, menuContent, e.clientX, e.clientY)} - className="collectionSchemaView-menuOptions-wrapper" - style={{ - background: col.color, padding: "2px", - display: "flex", cursor: "default", height: "100%", - }}> - {/* <FontAwesomeIcon onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> */} - {keysDropdown} - <div onClick={e => this.changeSorting(col)} - style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> - <FontAwesomeIcon icon={sortIcon} size="lg" /> - </div> - </div>; + const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> + {keysDropdown} + <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + <FontAwesomeIcon icon={sortIcon} size="lg" /> + </div> + </div>; return { Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, - accessor: (doc: Doc) => doc ? doc[col.heading] : 0, + accessor: (doc: Doc) => doc ? Field.toString(doc[col.heading] as Field) : 0, id: col.heading, Cell: (rowProps: CellInfo) => { const rowIndex = rowProps.index; @@ -258,21 +230,22 @@ export class SchemaTable extends React.Component<SchemaTableProps> { showDoc: this.showDoc, }; - const colType = this.getColumnType(col); - if (colType === ColumnType.Number) return <CollectionSchemaNumberCell {...props} />; - if (colType === ColumnType.String) return <CollectionSchemaStringCell {...props} />; - if (colType === ColumnType.Boolean) return <CollectionSchemaCheckboxCell {...props} />; - if (colType === ColumnType.Doc) return <CollectionSchemaDocCell {...props} />; - if (colType === ColumnType.Image) return <CollectionSchemaImageCell {...props} />; - if (colType === ColumnType.List) return <CollectionSchemaListCell {...props} />; - if (colType === ColumnType.Date) return <CollectionSchemaDateCell {...props} />; - return <CollectionSchemaCell {...props} />; + + switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) { + case ColumnType.Number: return <CollectionSchemaNumberCell {...props} />; + case ColumnType.String: return <CollectionSchemaStringCell {...props} />; + case ColumnType.Boolean: return <CollectionSchemaCheckboxCell {...props} />; + case ColumnType.Doc: return <CollectionSchemaDocCell {...props} />; + case ColumnType.Image: return <CollectionSchemaImageCell {...props} />; + case ColumnType.List: return <CollectionSchemaListCell {...props} />; + case ColumnType.Date: return <CollectionSchemaDateCell {...props} />; + default: + return <CollectionSchemaCell {...props} />; + } }, minWidth: 200, }; - }); - columns.push(...cols); - + })); columns.push({ Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, accessor: (doc: Doc) => 0, @@ -281,8 +254,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const rowIndex = rowProps.index; const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!); const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - const props: CellProps = { - row: rowIndex, + return <CollectionSchemaButtons {...{ + row: rowProps.index, col: columnIndex, rowProps: rowProps, isFocused: isFocused, @@ -301,9 +274,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed: this.setComputed, getField: this.getField, showDoc: this.showDoc, - }; - - return <CollectionSchemaButtons {...props} />; + }} />; }, width: 28, resizable: false @@ -314,14 +285,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { constructor(props: SchemaTableProps) { super(props); - // convert old schema columns (list of strings) into new schema columns (list of schema header fields) - const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); - if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { - const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); - } else if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb"), - new SchemaHeaderField("text", "#f1efeb"), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb")]); + if (this.props.Document._schemaHeaders === undefined) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb"), new SchemaHeaderField("author", "#f1efeb"), new SchemaHeaderField("*lastModified", "#f1efeb", ColumnType.Date), + new SchemaHeaderField("text", "#f1efeb", ColumnType.String), new SchemaHeaderField("type", "#f1efeb"), new SchemaHeaderField("context", "#f1efeb", ColumnType.Doc)]); } } @@ -334,7 +300,15 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + const tableDoc = this.props.Document[DataSym]; + const effectiveAcl = GetEffectiveAcl(tableDoc); + + if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) { + doc.context = this.props.Document; + tableDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())); + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + return false; } private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { @@ -360,19 +334,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true); // TODO: editing border doesn't work :( return { - style: { - border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" - } + style: { border: !this.props.headerIsEditing && isFocused ? "2px solid rgb(255, 160, 160)" : "1px solid #f1efeb" } }; } - @action - onCloseCollection = (collection: Doc): void => { - const index = this._openCollections.findIndex(col => col === collection[Id]); - if (index > -1) this._openCollections.splice(index, 1); - } - - @action onExpandCollection = (collection: Doc) => this._openCollections.push(collection[Id]); @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing; @action @@ -383,6 +348,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); pdoc && this.props.setPreviewDoc(pdoc); + e.stopPropagation(); + } else if (e.keyCode === 27) { + this.props.setPreviewDoc(undefined); + e.stopPropagation(); // stopPropagation for left/right arrows } } @@ -406,9 +375,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @undoBatch - createRow = () => { - this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - } + createRow = action(() => { + this.props.addDocument(Docs.Create.TextDocument("", { title: "", _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 100, _height: 30 })); + this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col }; + }); @undoBatch @action @@ -423,22 +393,21 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - getColumnType = (column: SchemaHeaderField): ColumnType => { - // added functionality to convert old column type stuff to new column type stuff -syip + getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => { + if (doc && field && column.type === ColumnType.Any) { + const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)]; + if (val instanceof ImageField) return ColumnType.Image; + if (val instanceof Doc) return ColumnType.Doc; + if (val instanceof DateField) return ColumnType.Date; + if (val instanceof List) return ColumnType.List; + } if (column.type && column.type !== 0) { return column.type; } if (columnTypes.get(column.heading)) { - column.type = columnTypes.get(column.heading)!; - return columnTypes.get(column.heading)!; - } - const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc)); - if (!typesDoc) { - column.type = ColumnType.Any; - return ColumnType.Any; + return column.type = columnTypes.get(column.heading)!; } - column.type = NumCast(typesDoc[column.heading]); - return NumCast(typesDoc[column.heading]); + return column.type = ColumnType.Any; } @undoBatch @@ -467,11 +436,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @computed get reactTable() { const children = this.childDocs; - const hasCollectionChild = children.reduce((found, doc) => found || doc.type === "collection", false); - const expandedRowsList = this._openCollections.map(col => children.findIndex(doc => doc[Id] === col).toString()); - const expanded = {}; - //@ts-ignore - expandedRowsList.forEach(row => expanded[row] = true); + const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false); + const expanded: { [name: string]: any } = {}; + Array.from(this._openCollections.keys()).map(col => expanded[col.toString()] = true); const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( return <ReactTable @@ -489,17 +456,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { expanded={expanded} resized={this.resized} onResizedChange={this.props.onResizedChange} - SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) : + SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== DocumentType.COL) ? (null) : <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} /></div>} />; } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); - } + ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" }); } getField = (row: number, col?: number) => { @@ -560,10 +524,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> { setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => { script = `const $ = (row:number, col?:number) => { - if(col === undefined) { - return (doc as any)[key][row + ${row}]; - } - return (doc as any)[key][row + ${row}][(doc as any)._schemaHeaders[col + ${col}].heading]; + const rval = (doc as any)[key][row + ${row}]; + return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading]; } return ${script}`; const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) }); @@ -583,9 +545,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } onOpenClick = () => { - if (this._showDoc) { - this.props.addDocTab(this._showDoc, "onRight"); - } + this._showDoc && this.props.addDocTab(this._showDoc, "add:right"); } getPreviewTransform = (): Transform => { @@ -594,13 +554,14 @@ export class SchemaTable extends React.Component<SchemaTableProps> { render() { const preview = ""; - return <div className="collectionSchemaView-table" onPointerDown={this.props.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} + return <div className="collectionSchemaView-table" + onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {StrCast(this.props.Document.type) !== "search" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + {StrCast(this.props.Document._chromeStatus) !== "disabled" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> : undefined} {!this._showDoc ? (null) : - <div className="collectionSchemaView-documentPreview" //onClick={() => { this.onOpenClick(); }} + <div className="collectionSchemaView-documentPreview" style={{ position: "absolute", width: 150, height: 150, background: "dimGray", display: "block", top: 0, left: 0, @@ -621,6 +582,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { PanelHeight={() => 150} ScreenToLocalTransform={this.getPreviewTransform} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss new file mode 100644 index 000000000..fdb801e03 --- /dev/null +++ b/src/client/views/collections/TabDocView.scss @@ -0,0 +1,22 @@ +input.lm_title:focus { + max-width: max-content !important; +} +.miniMap { + position: absolute; + overflow: hidden; + right: 10; + bottom: 10; + border: solid 1px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + + .miniOverlay { + width: 100%; + height: 100%; + position: absolute; + + .miniThumb { + background: #25252525; + position: absolute; + } + } +}
\ No newline at end of file diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx new file mode 100644 index 000000000..fb4f5c366 --- /dev/null +++ b/src/client/views/collections/TabDocView.tsx @@ -0,0 +1,383 @@ +import 'golden-layout/src/css/goldenlayout-base.css'; +import 'golden-layout/src/css/goldenlayout-dark-theme.css'; +import { clamp } from 'lodash'; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import * as ReactDOM from 'react-dom'; +import { DataSym, Doc, DocListCast, Opt } from "../../../fields/Doc"; +import { Id } from '../../../fields/FieldSymbols'; +import { FieldId } from "../../../fields/RefField"; +import { listSpec } from '../../../fields/Schema'; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocServer } from "../../DocServer"; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { DocumentManager } from '../../util/DocumentManager'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { PresBox } from '../nodes/PresBox'; +import { CollectionDockingView } from './CollectionDockingView'; +import "./TabDocView.scss"; +import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionViewType } from './CollectionView'; +import React = require("react"); +const _global = (window /* browser */ || global /* node */) as any; + +interface TabDocViewProps { + documentId: FieldId; + glContainer: any; +} +@observer +export class TabDocView extends React.Component<TabDocViewProps> { + _mainCont: HTMLDivElement | null = null; + _tabReaction: IReactionDisposer | undefined; + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _isActive: boolean = false; + @observable private _document: Doc | undefined; + @observable private _view: DocumentView | undefined; + + get stack(): any { return (this.props as any).glContainer.parent.parent; } + get tab() { return (this.props as any).glContainer.tab; } + get view() { return this._view; } + + @action + init = (tab: any, doc: Opt<Doc>) => { + if (tab.DashDoc !== doc && doc && tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + tab._disposers = {} as { [name: string]: IReactionDisposer }; + tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); + tab.DashDoc = doc; + CollectionDockingView.Instance.tabMap.add(tab); + + // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. + const titleEle = tab.titleElement[0]; + titleEle.size = StrCast(doc.title).length + 3; + titleEle.value = doc.title; + titleEle.style["max-width"] = "100px"; + titleEle.onchange = (e: any) => { + titleEle.size = e.currentTarget.value.length + 3; + Doc.GetProto(doc).title = e.currentTarget.value; + }; + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: MouseEvent) => { + if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + tab.header.parent.setActiveContentItem(tab.contentItem); + console.log("Seetting " + titleEle.value); + tab.setActive(true); + } + }; + const onPointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e) => { + !e.defaultPrevented && DragManager.StartDocumentDrag([dragHdl], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY); + return !e.defaultPrevented; + }, returnFalse, emptyFunction); + }; + + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected + tab.element[0].onclick = (e: any) => { + if (e.target.className !== "lm_close_tab" && this.view) { + SelectionManager.SelectDoc(this.view, false); + if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); + titleEle.lastClick = Date.now(); + (document.activeElement !== titleEle) && titleEle.focus(); + } + }; + tab._disposers.selectionDisposer = reaction(() => SelectionManager.SelectedDocuments().some(v => v.topMost && v.props.Document === doc), + (selected) => selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && + UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch")); + + //attach the selection doc buttons menu to the drag handle + const stack = tab.contentItem.parent; + const dragHdl = document.createElement("span"); + dragHdl.className = "collectionDockingView-gear"; + dragHdl.style.position = "relative"; + dragHdl.style.paddingLeft = "0px"; + dragHdl.style.paddingRight = "12px"; + tab._disposers.buttonDisposer = reaction(() => this.view, (view) => view && + [ReactDOM.render( + <span title="Drag as document" className="collectionDockingView-drag" onPointerDown={onPointerDown} > + <CollectionDockingViewMenu views={() => [view]} Stack={stack} /> + </span>, dragHdl), + tab._disposers.buttonDisposer?.()], + { fireImmediately: true }); + tab.reactComponents = [dragHdl]; + tab.element.append(dragHdl); + + // highlight the tab when the tab document is brushed in any part of the UI + tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { + titleEle.value = title; + titleEle.style.padding = degree ? 0 : 2; + titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + }, { fireImmediately: true }); + + // clean up the tab when it is closed + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", doc, undefined, true, true); + SelectionManager.DeselectAll(); + tab.contentItem.remove(); + }); + } + } + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static PinDoc(doc: Doc, unpin = false) { + if (unpin) TabDocView.UnpinDoc(doc); + else { + //add this new doc to props.Document + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presZoomButton = true; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + if (!DocumentManager.Instance.getDocumentView(curPres)) { + CollectionDockingView.AddSplit(curPres, "right"); + } + DocumentManager.Instance.jumpToDocument(doc, false, undefined, Cast(doc.context, Doc, null)); + } + } + } + /** + * Adds a document to the presentation view + **/ + @undoBatch + @action + public static UnpinDoc(doc: Doc) { + //add this new doc to props.Document + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); + ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); + } + } + + componentDidMount() { + const color = () => StrCast(this._document?._backgroundColor, this._document && CollectionDockingView.Instance?.props.backgroundColor?.(this._document, 0) || "white"); + const selected = () => SelectionManager.SelectedDocuments().some(v => v.props.Document === this._document); + const updateTabColor = () => this.tab?.titleElement[0] && (this.tab.titleElement[0].style.backgroundColor = selected() ? color() : ""); + const observer = new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + updateTabColor(); + })); + observer.observe(this.props.glContainer._element[0]); + this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(); + this._tabReaction = reaction(() => ({ views: SelectionManager.SelectedDocuments(), color: color() }), updateTabColor, { fireImmediately: true }); + } + + componentWillUnmount() { + this._tabReaction?.(); + this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); + } + + @action.bound + private onActiveContentItemChanged() { + if (this.props.glContainer.tab && this._isActive !== this.props.glContainer.tab.isActive) { + this._isActive = this.props.glContainer.tab.isActive; + this._isActive && setTimeout(() => this.view && SelectionManager.SelectDoc(this.view, false), 0); + (CollectionDockingView.Instance as any)._goldenLayout?.isInitialised && CollectionDockingView.Instance.stateChanged(); + !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. + } + } + + get layoutDoc() { return this._document && Doc.Layout(this._document); } + nativeAspect = () => this.nativeWidth() ? this.nativeWidth() / this.nativeHeight() : 0; + panelWidth = () => this.layoutDoc?.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : + (this.nativeAspect() && this.nativeAspect() < this._panelWidth / this._panelHeight ? this._panelHeight * this.nativeAspect() : this._panelWidth) + panelHeight = () => this.nativeAspect() && this.nativeAspect() > this._panelWidth / this._panelHeight ? this._panelWidth / this.nativeAspect() : this._panelHeight; + nativeWidth = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeWidth) || this._panelWidth : 0; + nativeHeight = () => !this.layoutDoc?._fitWidth ? NumCast(this.layoutDoc?._nativeHeight) || this._panelHeight : 0; + + contentScaling = () => { + const nativeH = this.nativeHeight(); + const nativeW = this.nativeWidth(); + let scaling = 1; + if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { + scaling = 1; + } else if (NumCast(this.layoutDoc?._nativeWidth) && ((this.layoutDoc?._fitWidth) || + this._panelHeight / NumCast(this.layoutDoc?._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc?._nativeWidth))) { + scaling = this._panelWidth / NumCast(this.layoutDoc?._nativeWidth); + } else if (nativeW && nativeH) { + const wscale = this.panelWidth() / nativeW; + scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; + } + return scaling; + } + + ScreenToLocalTransform = () => { + if (this._mainCont?.children) { + const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0]?.firstChild as HTMLElement); + const scale = Utils.GetScreenTransform(this._mainCont).scale; + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale); + } + return Transform.Identity(); + } + get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; } + get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this._panelWidth * 100}% ` : undefined; } + + // adds a tab to the layout based on the locaiton parameter which can be: + // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, + // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right + // replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right + // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack + addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => { + SelectionManager.DeselectAll(); + if (doc._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + const locationFields = location.split(":"); + const locationParams = locationFields.length > 1 ? locationFields[1] : ""; + switch (locationFields[0]) { + case "close": return CollectionDockingView.CloseSplit(doc, locationParams); + case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); + case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); + case "inPlace": + case "add": + default: return CollectionDockingView.AddSplit(doc, locationParams, this.stack); + } + } + + @computed get renderContentBounds() { + const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + const xbounds = bounds[2] - bounds[0]; + const ybounds = bounds[3] - bounds[1]; + const dim = Math.max(xbounds, ybounds); + return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + } + @computed get miniLeft() { return 50 + (NumCast(this._document?._panX) - this.renderContentBounds.cx) / this.renderContentBounds.dim * 100 - this.miniWidth / 2; } + @computed get miniTop() { return 50 + (NumCast(this._document?._panY) - this.renderContentBounds.cy) / this.renderContentBounds.dim * 100 - this.miniHeight / 2; } + @computed get miniWidth() { return this.panelWidth() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } + @computed get miniHeight() { return this.panelHeight() / NumCast(this._document?._viewScale, 1) / this.renderContentBounds.dim * 100; } + childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + this._document && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + this._document!._panX = clamp(NumCast(this._document!._panX) + delta[0] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.l, this.renderContentBounds.l + this.renderContentBounds.dim); + this._document!._panY = clamp(NumCast(this._document!._panY) + delta[1] / this.returnMiniSize() * this.renderContentBounds.dim, this.renderContentBounds.t, this.renderContentBounds.t + this.renderContentBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + getCurrentFrame = (): number => { + const presTargetDoc = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null); + return Cast(presTargetDoc._currentFrame, "number", null); + } + + renderMiniMap() { + return <div className="miniMap" style={{ + width: this.returnMiniSize(), height: this.returnMiniSize(), background: StrCast(this._document!._backgroundColor, + StrCast(this._document!.backgroundColor, CollectionDockingView.Instance.props.backgroundColor?.(this._document!, 0))), + }}> + <CollectionFreeFormView + Document={this._document!} + LibraryPath={emptyPath} + CollectionView={undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ChildLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid havin to set stuff like this. + noOverlay={true} // don't render overlay Docs since they won't scale + active={returnTrue} + select={emptyFunction} + dropAction={undefined} + isSelected={returnFalse} + dontRegisterView={true} + annotationsKey={""} + fieldKey={Doc.LayoutFieldKey(this._document!)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + ContentScaling={returnOne} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + NativeHeight={returnZero} + NativeWidth={returnZero} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + whenActiveChanged={emptyFunction} + focus={emptyFunction} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + fitToBox={true} + /> + <div className="miniOverlay" onPointerDown={this.miniDown} > + <div className="miniThumb" style={{ + width: `${this.miniWidth}% `, + height: `${this.miniHeight}% `, + left: `${this.miniLeft}% `, + top: `${this.miniTop}% `, + }} + /> + </div> + </div>; + } + focusFunc = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => void) => afterFocus?.(); + setView = action((view: DocumentView) => this._view = view); + @computed get docView() { + TraceMobx(); + return !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : + <><DocumentView key={this._document[Id]} + LibraryPath={emptyPath} + Document={this._document} + getView={this.setView} + DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={undefined} + removeDocument={undefined} + ContentScaling={this.contentScaling} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={this.nativeHeight} + NativeWidth={this.nativeWidth} + ScreenToLocalTransform={this.ScreenToLocalTransform} + renderDepth={0} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + focus={this.focusFunc} + backgroundColor={CollectionDockingView.Instance.props.backgroundColor} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + {this._document._viewType === CollectionViewType.Freeform && !this._document?.hideMinimap ? this.renderMiniMap() : (null)} + </>; + } + + render() { + return (<div className="collectionDockingView-content" ref={ref => { + if (this._mainCont = ref) { + (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); + this.tab && DocServer.GetRefField(this.tab.contentItem.config.props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.init(this.tab, this._document))); + } + }} + style={{ + transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`, + height: this.layoutDoc?._fitWidth ? undefined : "100%", + width: this.widthpercent + }}> + {this.docView} + </div >); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index b00074cc6..bc2cb2d20 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -10,6 +10,7 @@ import { Id, ToString } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { RefField } from "../../../../fields/RefField"; import { listSpec } from "../../../../fields/Schema"; +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export interface ViewDefBounds { type: string; @@ -359,7 +360,7 @@ export function computeTimelineLayout( groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined }); } - const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; + const divider = { type: "div", color: CurrentUserUtils.ActiveDashboard?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined }; return normalizeResults(panelDim, fontHeight, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]); function layoutDocsAtTime(keyDocs: Doc[], key: number) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 46e30f616..306dfe797 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,7 +1,4 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; -import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; @@ -15,7 +12,7 @@ import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, Utils, setupMoveUpEvents } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -24,10 +21,12 @@ import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; +import { LinkManager } from "../../../util/LinkManager"; +import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; @@ -37,29 +36,25 @@ import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; +import { PresBox } from "../../nodes/PresBox"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; import { CollectionViewType } from "../CollectionView"; import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines"; import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors"; import "./CollectionFreeFormView.scss"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { PresBox } from "../../nodes/PresBox"; -import { SearchUtil } from "../../../util/SearchUtil"; -import { LinkManager } from "../../../util/LinkManager"; - -library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); export const panZoomSchema = createSchema({ _panX: "number", _panY: "number", - currentTimecode: "number", + _currentTimecode: "number", displayTimecode: "number", - currentFrame: "number", + _currentFrame: "number", arrangeInit: ScriptField, - useClusters: "boolean", + _useClusters: "boolean", fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set @@ -178,8 +173,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newBox.y = y; } } - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { - CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document.currentFrame); + if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { + CollectionFreeFormDocumentView.setupKeyframes(newBoxes, this.Document._currentFrame, true); } } return retVal; @@ -189,7 +184,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } - public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } + public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document._currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -212,9 +207,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined) { + if (this.Document._currentFrame !== undefined) { const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, vals.opacity); + CollectionFreeFormDocumentView.setValues(this.Document._currentFrame, d, x + vals.x - dropPos[0], y + vals.y - dropPos[1], vals.h, vals.w, vals.scroll, vals.opacity); } else { d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; @@ -222,7 +217,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300); - d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront + d._isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront } (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); @@ -261,7 +256,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - // if (this.props.Document.isBackground) return false; + // if (this.props.Document._isBackground) return false; const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) { return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); @@ -306,8 +301,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch @action - updateClusters(useClusters: boolean) { - this.props.Document.useClusters = useClusters; + updateClusters(_useClusters: boolean) { + this.props.Document._useClusters = _useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -315,7 +310,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + if (this.props.Document._useClusters) { const docFirst = docs[0]; docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); const preferredInd = NumCast(docFirst.cluster); @@ -325,7 +320,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docFirst.cluster = i; } }))); - if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { docFirst.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -338,7 +333,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); - } else { + } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc)); } @@ -350,7 +345,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document.useClusters) { + if (this.props.Document._useClusters) { this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); const preferredInd = NumCast(doc.cluster); doc.cluster = -1; @@ -359,7 +354,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.cluster = i; } })); - if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { doc.cluster = preferredInd; } this._clusterSets.map((set, i) => { @@ -370,7 +365,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (doc.cluster === -1) { doc.cluster = this._clusterSets.length; this._clusterSets.push([doc]); - } else { + } else if (this._clusterSets.length) { for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); this._clusterSets[doc.cluster].push(doc); } @@ -380,7 +375,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getClusterColor = (doc: Doc) => { let clusterColor = this.props.backgroundColor?.(doc, this.props.renderDepth + 1); const cluster = NumCast(doc.cluster); - if (this.Document.useClusters) { + if (this.Document._useClusters) { if (this._clusterSets.length <= cluster) { setTimeout(() => this.updateCluster(doc), 0); } else { @@ -389,8 +384,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P clusterColor = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); - set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => !s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); + set && set.filter(s => s._isBackground).map(s => clusterColor = StrCast(s.backgroundColor)); } } return clusterColor; @@ -402,7 +397,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { return; } - this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; + this._hitCluster = this.props.Document._useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) { // if (!this.props.Document.aliasOf && !this.props.ContainingCollectionView) { @@ -591,7 +586,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if (this.layoutDoc.targetScale && (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { if (Date.now() - this._lastTap < 300) { - runInAction(() => DocumentLinksButton.StartLink = undefined); + runInAction(() => { + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + }); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); @@ -605,7 +603,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 - // PDFMenu.Instance.fadeOut(true); (commented out for mobile) const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); @@ -641,7 +638,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { - // panning a workspace if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; @@ -843,14 +839,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } bringToFront = action((doc: Doc, sendToBack?: boolean) => { - if (sendToBack || doc.isBackground) { + if (sendToBack || doc._isBackground) { doc.zIndex = 0; } else if (doc.isInkMask) { doc.zIndex = 5000; } else { const docs = this.childLayoutPairs.map(pair => pair.layout); docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - let zlast = docs.length ? NumCast(docs[docs.length - 1].zIndex) : 1; + let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1; if (zlast - docs.length > 100) { for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1; zlast = docs.length + 1; @@ -908,10 +904,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; - // if (!willZoom && DocumentView._focusHack.length) { - // Doc.BrushDoc(this.props.Document); - // !doc.z && NumCast(this.layoutDoc.scale) < 1 && this.scaleAtPt(DocumentView._focusHack, 1); // [NumCast(doc.x), NumCast(doc.y)], 1); - // } else { if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (!doc.z) this.setPan(newPanX, newPanY, doc.presTransition || doc.presTransition === 0 ? `transform ${doc.presTransition}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow @@ -920,7 +912,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.focus(this.props.Document); willZoom && this.setScaleToZoom(layoutdoc, scale); Doc.linkFollowHighlight(doc); - //} afterFocus && setTimeout(() => { if (afterFocus?.()) { @@ -939,10 +930,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } + @computed get backgroundActive() { return this.layoutDoc._isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); } onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); - backgroundHalo = () => BoolCast(this.Document.useClusters); + backgroundHalo = () => BoolCast(this.Document._useClusters); parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { @@ -975,6 +966,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, docFilters: this.docFilters, + searchFilterDocs: this.searchFilterDocs, focus: this.focusDocument, backgroundColor: this.getClusterColor, backgroundHalo: this.backgroundHalo, @@ -1008,8 +1000,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); - const { x, y, opacity } = this.Document.currentFrame === undefined ? params.pair.layout : - CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.currentFrame || 0); + const { x, y, opacity } = this.Document._currentFrame === undefined ? params.pair.layout : + CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); const { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), @@ -1045,7 +1037,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else if (viewDef.type === "div") { return [x, y].some(val => val === undefined) ? undefined : { - ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)} + ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z + viewDef.payload} onClick={e => this.onViewDefDivClick(e, viewDef)} style={{ width, height, backgroundColor: color, transform }} />, bounds: viewDef }; @@ -1140,7 +1132,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); } - this.Document.useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); + this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } @@ -1175,7 +1167,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (this._marqueeRef?.current) { + if (!this.props.Document._noAutoscroll && this._marqueeRef?.current) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef.current?.getBoundingClientRect(); @@ -1252,15 +1244,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null; - !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }) : null; + !Doc.UserDoc().noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && - optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: faEye }); + optionItems.push({ description: (this.showTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this.showTimeline = !this.showTimeline), icon: "eye" }); this.props.ContainingCollectionView && - optionItems.push({ description: "Undo Collection", event: this.promoteCollection, icon: "table" }); + optionItems.push({ description: "Move Items Out of Collection", event: this.promoteCollection, icon: "table" }); optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); if (!Doc.UserDoc().noviceMode) { @@ -1315,7 +1307,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @action - setupDragLines = () => { + setupDragLines = (snapToDraggedDoc: boolean = false) => { const activeDocs = this.getActiveDocuments(); if (activeDocs.length > 50) { DragManager.SetSnapLines([], []); @@ -1331,13 +1323,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }; const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to; const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) }; - this.getActiveDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to + this.getActiveDocuments().filter(doc => !doc._isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs const horizLines: number[] = []; const vertLines: number[] = []; - snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { + snappableDocs.filter(doc => snapToDraggedDoc || !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top); const docSize = this.getTransform().inverse().transformDirection(width, height); @@ -1349,7 +1341,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onPointerOver = (e: React.PointerEvent) => { if (SnappingManager.GetIsDragging()) { - this.setupDragLines(); + this.setupDragLines(e.ctrlKey || e.shiftKey); } e.stopPropagation(); } @@ -1406,12 +1398,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P centeringShiftY={this.centeringShiftY} presPaths={BoolCast(this.Document.presPathView)} progressivize={BoolCast(this.Document.editProgressivize)} - zoomProgressivize={BoolCast(this.Document.editZoomProgressivize)} + presPinView={BoolCast(this.Document.presPinView)} transition={Cast(this.layoutDoc._viewTransition, "string", null)} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> {this.children} - </CollectionFreeFormViewPannableContents></div> + </CollectionFreeFormViewPannableContents> + </div> {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @@ -1424,7 +1417,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - @computed get backgroundEvents() { return this.layoutDoc.isBackground && SnappingManager.GetIsDragging(); } + @computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); } render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1492,7 +1485,7 @@ interface CollectionFreeFormViewPannableContentsProps { transition?: string; presPaths?: boolean; progressivize?: boolean; - zoomProgressivize?: boolean; + presPinView?: boolean; } @observer @@ -1559,48 +1552,27 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF doc.style.top = toNumber(top, e.movementY) + 'px'; doc.style.left = toNumber(left, e.movementX) + 'px'; } - this.updateAll(height, width, top, left); + // this.updateAll(height, width, top, left); return false; } return true; } - @action - updateAll = (width: number, height: number, top: number, left: number) => { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - this.updateList(targetDoc, activeItem["viewfinder-width-indexed"], width); - this.updateList(targetDoc, activeItem["viewfinder-height-indexed"], height); - this.updateList(targetDoc, activeItem["viewfinder-top-indexed"], top); - this.updateList(targetDoc, activeItem["viewfinder-left-indexed"], left); - } - - @action - updateList = (doc: Doc, list: any, val: number) => { - const x: List<number> = list; - if (x && x.length >= NumCast(doc.currentFrame) + 1) { - x[NumCast(doc.currentFrame)] = val; - list = x; - } else if (doc && x) { - x.length = NumCast(doc.currentFrame) + 1; - x[NumCast(doc.currentFrame)] = val; - list = x; - } - } - // scale: NumCast(targetDoc._viewScale), @computed get zoomProgressivizeContainer() { - const activeItem = Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - if (activeItem && activeItem.zoomProgressivize) { - const vfLeft: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); - const vfWidth: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-width-indexed"]); - const vfTop: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-top-indexed"]); - const vfHeight: number = PresBox.Instance.checkList(targetDoc, activeItem["viewfinder-height-indexed"]); + const activeItem = PresBox.Instance.activeItem; + // const targetDoc = PresBox.Instance.targetDoc; + if (activeItem && activeItem.presPinView && activeItem.id) { + const vfLeft: number = NumCast(activeItem.presPinViewX); + const vfTop: number = NumCast(activeItem.presPinViewY); + const vfWidth: number = 100; + const vfHeight: number = 100; + console.log(vfTop + " | " + vfLeft); + console.log(this.props.presPinView); return ( <> - {!activeItem.editZoomProgressivize ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> - <div className='resizers'> + {!this.props.presPinView ? (null) : <div id="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> + <div className='resizers' key={'resizer' + activeItem.id}> <div id="resizer-tl" className='resizer top-left' onPointerDown={this.onPointerDown}></div> <div id="resizer-tr" className='resizer top-right' onPointerDown={this.onPointerDown}></div> <div id="resizer-bl" className='resizer bottom-left' onPointerDown={this.onPointerDown}></div> @@ -1613,7 +1585,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF } @computed get zoomProgressivize() { - return PresBox.Instance && this.props.zoomProgressivize ? this.zoomProgressivizeContainer : (null); + return PresBox.Instance && PresBox.Instance.activeItem && PresBox.Instance.activeItem.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); } @computed get progressivize() { @@ -1622,30 +1594,30 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF @computed get presPaths() { const presPaths = "presPaths" + (this.props.presPaths ? "" : "-hidden"); - return !(PresBox.Instance) ? (null) : (<> - {!this.props.presPaths ? (null) : <><div>{PresBox.Instance.order}</div> - <svg className={presPaths}> - <defs> - <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" /> - </marker> - <marker id="square" markerWidth="3" markerHeight="3" overflow="visible" - refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> - <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" /> - </marker> - <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4" - orient="auto" overflow="visible"> - <rect x="1" y="1" width="5" height="5" fill="#69a6db" /> - </marker> - - <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7" - orient="auto" overflow="visible"> - <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" /> - </marker> - </defs>; - {PresBox.Instance.paths} - </svg></>} - </>); + return !PresBox.Instance || !this.props.presPaths ? (null) : <> + <div key="presorder">{PresBox.Instance.order}</div> + <svg key="svg" className={presPaths}> + <defs> + <marker id="arrow" markerWidth="3" overflow="visible" markerHeight="3" refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,6 L9,3 z" fill="#69a6db" /> + </marker> + <marker id="square" markerWidth="3" markerHeight="3" overflow="visible" + refX="5" refY="5" orient="auto" markerUnits="strokeWidth"> + <path d="M 5,1 L 9,5 5,9 1,5 z" fill="#69a6db" /> + </marker> + <marker id="markerSquare" markerWidth="7" markerHeight="7" refX="4" refY="4" + orient="auto" overflow="visible"> + <rect x="1" y="1" width="5" height="5" fill="#69a6db" /> + </marker> + + <marker id="markerArrow" markerWidth="5" markerHeight="5" refX="2" refY="7" + orient="auto" overflow="visible"> + <path d="M2,2 L2,13 L8,7 L2,2" fill="#69a6db" /> + </marker> + </defs> + {PresBox.Instance.paths} + </svg> + </>; } render() { @@ -1659,7 +1631,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF return <div className={freeformclass} style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, - transition: this.props.transition + transition: this.props.transition, + //willChange: "transform" }}> {this.props.children()} {this.presPaths} diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss deleted file mode 100644 index d49ab27fb..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss +++ /dev/null @@ -1,68 +0,0 @@ -.antimodeMenu-button { - width: 200px; - position: relative; - text-align: left; - - .color-previewI { - width: 100%; - height: 40%; - } - - .color-previewII { - width: 100%; - height: 100%; - } -} - -.antimenu-Buttonup { - position: absolute; - width: 20; - height: 10; - right: 0; - padding: 0; -} - -.formatShapePane-inputBtn { - width: inherit; - position: absolute; -} - -.btn-group-palette { - .sketch-picker { - background: #323232; - width: 160px !important; - height: 80% !important; - - .flexbox-fit { - background: #323232; - } - } -} - -.btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - /* Make the buttons appear below each other */ -} - -.btn-group-palette { - display: block; - /* Make the buttons appear below each other */ -} - -.btn-draw { - display: inline; - /* Make the buttons appear below each other */ -} - -.btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx deleted file mode 100644 index 1ffa2fbed..000000000 --- a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx +++ /dev/null @@ -1,558 +0,0 @@ -import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc, Field, Opt } from "../../../../fields/Doc"; -import { Document } from "../../../../fields/documentSchemas"; -import { InkField } from "../../../../fields/InkField"; -import { BoolCast, Cast, NumCast } from "../../../../fields/Types"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { SelectionManager } from "../../../util/SelectionManager"; -import AntimodeMenu from "../../AntimodeMenu"; -import "./FormatShapePane.scss"; -import { undoBatch } from "../../../util/UndoManager"; -import { ColorState, SketchPicker } from 'react-color'; - -@observer -export default class FormatShapePane extends AntimodeMenu { - static Instance: FormatShapePane; - - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash = "2"; - private _mode = ["fill-drip", "ruler-combined"]; - - @observable private _subOpen = [false, false]; - @observable private _currMode = "fill-drip"; - @observable _lock = false; - @observable private _fillBtn = false; - @observable private _lineBtn = false; - @observable _controlBtn = false; - @observable private _controlPoints: { X: number, Y: number }[] = []; - @observable _currPoint = -1; - - getField(key: string) { - return this.selectedInk?.reduce((p, i) => - (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt<string>); - } - - @computed get selectedInk() { - const inks = SelectionManager.SelectedDocuments().filter(i => Document(i.rootDoc).type === DocumentType.INK); - return inks.length ? inks : undefined; - } - @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - set solidFil(value) { this.unFilled = !value; } - set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - } - set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - set shapeWid(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - i.rootDoc._width = Number(value); - this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - }); - } - set shapeHgt(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldHeight = NumCast(i.rootDoc._height); - i.rootDoc._height = Number(value); - this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - }); - } - - constructor(props: Readonly<{}>) { - super(props); - FormatShapePane.Instance = this; - this._canFade = false; - this.Pinned = BoolCast(Doc.UserDoc()["menuFormatShape-pinned"]); - } - - @action - closePane = () => { - this.fadeOut(false); - this.Pinned = false; - } - - @action - upDownButtons = (dirs: string, field: string) => { - switch (field) { - case "rot": this.rotate((dirs === "up" ? .1 : -.1)); break; - // case "rot": this.selectedInk?.forEach(i => i.rootDoc.rotation = NumCast(i.rootDoc.rotation) + (dirs === "up" ? 0.1 : -0.1)); break; - case "Xps": this.selectedInk?.forEach(i => i.rootDoc.x = NumCast(i.rootDoc.x) + (dirs === "up" ? 10 : -10)); break; - case "Yps": this.selectedInk?.forEach(i => i.rootDoc.y = NumCast(i.rootDoc.y) + (dirs === "up" ? 10 : -10)); break; - case "stk": this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = NumCast(i.rootDoc.strokeWidth) + (dirs === "up" ? .1 : -.1)); break; - case "wid": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - //redraw points - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); - i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - const oldHeight = NumCast(i.rootDoc._height); - const oldX = NumCast(i.rootDoc.x); - const oldY = NumCast(i.rootDoc.y); i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10); - this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width))); - const doc = Document(i.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - console.log(doc.x, doc.y, doc._height, doc._width); - const ink = Cast(doc.data, InkField)?.inkData; - console.log(ink); - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = ((doc.x || 0) - oldX) + (i.X * (doc._width || 0)) / oldWidth; - const newY = ((doc.y || 0) - oldY) + (i.Y * (doc._height || 0)) / oldHeight; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - } - } - }); - break; - } - } - - @undoBatch - @action - addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - for (var j = 0; j < 4; j++) { - newPoints.push({ X: x, Y: y }); - - } - for (var i = spNum; i < ink.length; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - } - } - } - })); - } - - @undoBatch - @action - deletePoints = () => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && this._currPoint !== -1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink && ink.length > 4) { - const newPoints: { X: number, Y: number }[] = []; - - console.log(ink.length, this._currPoint, Math.floor((this._currPoint + 2) / 4)); - - const toRemove = Math.floor(((this._currPoint + 2) / 4)); - for (var i = 0; i < ink.length; i++) { - if (Math.floor((i + 2) / 4) !== toRemove) { - console.log(i, toRemove); - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - this._currPoint = -1; - doc.data = new InkField(newPoints); - if (newPoints.length === 4) { - const newerPoints: { X: number, Y: number }[] = []; - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); - doc.data = new InkField(newerPoints); - - } - } - } - } - })); - } - - @undoBatch - @action - rotate = (angle: number) => { - const _centerPoints: { X: number, Y: number }[] = []; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - _centerPoints.push({ X: left, Y: top }); - } - } - })); - - var index = 0; - SelectionManager.SelectedDocuments().forEach(action(inkView => { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - doc.rotation = Number(doc.rotation) + Number(angle); - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - ink.forEach(i => { - const newX = Math.cos(angle) * (i.X - _centerPoints[index].X) - Math.sin(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].X; - const newY = Math.sin(angle) * (i.X - _centerPoints[index].X) + Math.cos(angle) * (i.Y - _centerPoints[index].Y) + _centerPoints[index].Y; - newPoints.push({ X: newX, Y: newY }); - }); - doc.data = new InkField(newPoints); - const xs = newPoints.map(p => p.X); - const ys = newPoints.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - const right = Math.max(...xs); - const bottom = Math.max(...ys); - - doc._height = (bottom - top); - doc._width = (right - left); - } - index++; - } - })); - } - - @undoBatch - @action - control = (xDiff: number, yDiff: number, controlNum: number) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - - const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; - for (var i = 0; i < ink.length; i++) { - if (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) - || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) { - newPoints.push({ X: ink[i].X - (xDiff * inkView.props.ScreenToLocalTransform().Scale), Y: ink[i].Y - (yDiff * inkView.props.ScreenToLocalTransform().Scale) }); - } - else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - const oldx = doc.x; - const oldy = doc.y; - const xs = ink.map(p => p.X); - const ys = ink.map(p => p.Y); - const left = Math.min(...xs); - const top = Math.min(...ys); - doc.data = new InkField(newPoints); - const xs2 = newPoints.map(p => p.X); - const ys2 = newPoints.map(p => p.Y); - const left2 = Math.min(...xs2); - const top2 = Math.min(...ys2); - const right2 = Math.max(...xs2); - const bottom2 = Math.max(...ys2); - doc._height = (bottom2 - top2); - doc._width = (right2 - left2); - //if points move out of bounds - - doc.x = oldx - (left - left2); - doc.y = oldy - (top - top2); - - } - } - } - })); - } - - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - } - - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } - - - colorPicker(setter: (color: string) => {}, type: string) { - return <div className="btn-group-palette" key="colorpicker" style={{ width: 160, margin: 10 }}> - <SketchPicker onChange={type === "stk" ? this.switchStk : this.switchFil} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={type === "stk" ? this.colorStk : this.colorFil} /> - </div>; - } - inputBox = (key: string, value: any, setter: (val: string) => {}) => { - return <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value} - onChange={undoBatch(action((e) => setter(e.target.value)))} - autoFocus /> - <button className="antiMenu-Buttonup" key="up1" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down1" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}> - ˅ - </button> - </>; - } - - inputBoxDuo = (key: string, value: any, setter: (val: string) => {}, title1: string, key2: string, value2: any, setter2: (val: string) => {}, title2: string) => { - return <> - {title1} - <p style={{ marginTop: -20, right: 70, position: "absolute" }}>{title2}</p> - - <input style={{ color: "black", width: 40, position: "absolute", right: 130 }} - type="text" value={value} - onChange={e => setter(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))} style={{ right: 110 }}> - ˄ - </button> - <button className="antiMenu-Buttonup" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: 12, right: 110 }}> - ˅ - </button> - {title2 === "" ? "" : <> - <input style={{ color: "black", width: 40, position: "absolute", right: 20 }} - type="text" value={value2} - onChange={e => setter2(e.target.value)} - autoFocus /> - <button className="antiMenu-Buttonup" key="up3" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key2)))}> - ˄ - </button> - <br /> - <button className="antiMenu-Buttonup" key="down3" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key2)))} style={{ marginTop: -8 }}> - ˅ - </button></>} - </>; - } - - - colorButton(value: string, setter: () => {}) { - return <> - <button className="antimodeMenu-button" key="color" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "relative", marginTop: -5 }}> - <div className="color-previewII" style={{ backgroundColor: value ?? "121212" }} /> - {value === "" || value === "transparent" ? <p style={{ fontSize: 25, color: "red", marginTop: -23, position: "fixed" }}>☒</p> : ""} - </button> - </>; - } - - controlPointsButton() { - return <> - <button className="antimodeMenu-button" title="Edit points" key="bezier" onPointerDown={action(() => this._controlBtn = this._controlBtn ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._controlBtn ? "black" : "" }}> - <FontAwesomeIcon icon="bezier-curve" size="lg" /> - </button> - <button className="antimodeMenu-button" title="Lock ratio" key="ratio" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "relative", marginTop: 10, backgroundColor: this._lock ? "black" : "" }}> - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <button className="antimodeMenu-button" key="rotate" title="Rotate 90˚" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "relative", marginTop: 10, fontSize: 15 }}> - ⟲ - </button> - <br /> <br /> - </>; - } - - lockRatioButton() { - return <> - <button className="antimodeMenu-button" key="lock" onPointerDown={action(() => this._lock = this._lock ? false : true)} style={{ position: "absolute", right: 80, backgroundColor: this._lock ? "black" : "" }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - <FontAwesomeIcon icon="lock" size="lg" /> - - </button> - <br /> <br /> - </>; - } - - rotate90Button() { - return <> - <button className="antimodeMenu-button" key="rot" onPointerDown={action(() => this.rotate(Math.PI / 2))} style={{ position: "absolute", right: 80, }}> - {/* <FontAwesomeIcon icon="bezier-curve" size="lg" /> */} - ⟲ - - </button> - <br /> <br /> - </>; - } - @computed get fillButton() { return this.colorButton(this.colorFil, () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; return true; }); } - @computed get lineButton() { return this.colorButton(this.colorStk, () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; return true; }); } - - @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color, "fil"); } - @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color, "stk"); } - - @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); } - @computed get dashInput() { return this.inputBox("dsh", this.widthStk, (val: string) => this.widthStk = val); } - - @computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val, "H:", "wid", this.shapeWid, (val: string) => this.shapeWid = val, "W:"); } - @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } - @computed get rotInput() { return this.inputBoxDuo("rot", this.shapeRot, (val: string) => { this.rotate(Number(val) - Number(this.shapeRot)); this.shapeRot = val; return true; }, "∠:", "rot", this.shapeRot, (val: string) => this.shapeRot = val, ""); } - - @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } - - @computed get controlPoints() { return this.controlPointsButton(); } - @computed get lockRatio() { return this.lockRatioButton(); } - @computed get rotate90() { return this.rotate90Button(); } - @computed get XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } - - - @computed get propertyGroupItems() { - const fillCheck = <div key="fill" style={{ display: (this._subOpen[0] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - Fill: - {this.fillButton} - <div style={{ float: "left", width: 100 }} > - Stroke: - {this.lineButton} - </div> - - {this._fillBtn ? this.fillPicker : ""} - {this._lineBtn ? this.linePicker : ""} - {this._fillBtn || this._lineBtn ? "" : <br />} - {(this.solidStk || this.dashdStk) ? "Width" : ""} - {(this.solidStk || this.dashdStk) ? this.stkInput : ""} - - - {(this.solidStk || this.dashdStk) ? <input type="range" defaultValue={Number(this.widthStk)} min={1} max={100} onChange={undoBatch(action((e) => this.widthStk = e.target.value))} /> : (null)} - <br /> - {(this.solidStk || this.dashdStk) ? <> - <p style={{ position: "absolute", fontSize: 12 }}>Arrow Head</p> - <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} style={{ position: "absolute", right: 110, width: 20 }} /> - <p style={{ position: "absolute", fontSize: 12, right: 30 }}>Arrow End</p> - <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} style={{ position: "absolute", right: 0, width: 20 }} /> - <br /> - </> : ""} - Dash: <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.dashdStk === "2"} onChange={undoBatch(action(() => this.dashdStk = this.dashdStk === "2" ? "0" : "2"))} style={{ position: "absolute", right: 110, width: 20 }} /> - - - - </div>; - - - - const sizeCheck = - - <div key="sizeCheck" style={{ display: (this._subOpen[1] && this.selectedInk && this.selectedInk.length >= 1) ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> - {this.controlPoints} - {this.hgtInput} - {this.XpsInput} - {this.rotInput} - - </div>; - - - const subMenus = this._currMode === "fill-drip" ? [`Appearance`, 'Transform'] : []; - const menuItems = this._currMode === "fill-drip" ? [fillCheck, sizeCheck] : []; - const indexOffset = 0; - - return <div className="antimodeMenu-sub" key="submenu" style={{ position: "absolute", width: "inherit", top: 60 }}> - {subMenus.map((subMenu, i) => - <div key={subMenu} style={{ width: "inherit" }}> - <button className="antimodeMenu-button" onPointerDown={action(() => this._subOpen[i + indexOffset] = !this._subOpen[i + indexOffset])} - style={{ backgroundColor: "121212", position: "relative", width: "inherit" }}> - {this._subOpen[i + indexOffset] ? "▼" : "▶︎"} - {subMenu} - </button> - {menuItems[i]} - </div>)} - </div>; - } - - @computed get closeBtn() { - return <button className="antimodeMenu-button" key="close" onPointerDown={action(() => this.closePane())} style={{ position: "absolute", right: 0 }}> - X - </button>; - } - - @computed get propertyGroupBtn() { - return <div className="antimodeMenu-button-tab" key="modes"> - {this._mode.map(mode => - <button className="antimodeMenu-button" key={mode} onPointerDown={action(() => this._currMode = mode)} - style={{ backgroundColor: this._currMode === mode ? "121212" : "", position: "relative", top: 30 }}> - <FontAwesomeIcon icon={mode as IconProp} size="lg" /> - </button>)} - </div>; - } - - render() { - return this.getElementVert([this.closeBtn, - this.propertyGroupItems]); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index f1df7998b..46298ec6f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -1,13 +1,12 @@ import React = require("react"); -import AntimodeMenu from "../../AntimodeMenu"; -import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction } from "../../../../Utils"; -import { undoBatch } from "../../../util/UndoManager"; import { Tooltip } from "@material-ui/core"; +import { observer } from "mobx-react"; +import { unimplementedFunction } from "../../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; @observer -export default class MarqueeOptionsMenu extends AntimodeMenu { +export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: MarqueeOptionsMenu; public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; @@ -16,6 +15,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; + public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; constructor(props: Readonly<{}>) { super(props); @@ -53,6 +53,14 @@ export default class MarqueeOptionsMenu extends AntimodeMenu { <FontAwesomeIcon icon="font" size="lg" /> </button> </Tooltip>, + <Tooltip key="pinWithView" title={<><div className="dash-tooltip">Pin to presentation with selected view</div></>} placement="bottom"> + <button + className="antimodeMenu-button" + onPointerDown={this.pinWithView}> + <FontAwesomeIcon icon="map-pin" size="lg" /> + <div style={{ position: 'relative', fontSize: 25, fontWeight: 700, transform: 'translate(-4px, -22px)', color: 'rgba(250,250,250,0.55)' }}>V</div> + </button> + </Tooltip>, ]; return this.getElement(buttons); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c0b19fcd2..d8e1bcc9c 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,27 +1,28 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; -import { GetEffectiveAcl } from "../../../../fields/util"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; +import { GetEffectiveAcl } from "../../../../fields/util"; import { Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { Docs, DocumentOptions, DocUtils } from "../../../documents/Documents"; +import { DocumentManager } from "../../../util/DocumentManager"; import { SelectionManager } from "../../../util/SelectionManager"; import { Transform } from "../../../util/Transform"; -import { undoBatch } from "../../../util/UndoManager"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { ContextMenu } from "../../ContextMenu"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { PreviewCursor } from "../../PreviewCursor"; +import { CollectionDockingView } from "../CollectionDockingView"; import { SubCollectionViewProps } from "../CollectionSubView"; -import { CollectionView } from "../CollectionView"; -import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; +import { CollectionView, CollectionViewType } from "../CollectionView"; +import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); -import { ContextMenuItem } from "../../ContextMenuItem"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -75,7 +76,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); if (e.key === "?") { cm.setDefaultItem("?", (str: string) => this.props.addDocTab( - Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false, title: "bing", UseCors: true }), "onRight")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -130,6 +131,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), _fontFamily: StrCast(Doc.UserDoc().fontFamily), + _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, title: "-typed text-" }); const template = FormattedTextBox.DefaultLayout; @@ -138,6 +140,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque tbox.layoutKey = "layout_" + StrCast(template.title); Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); this.props.addLiveTextDocument(tbox); e.stopPropagation(); } @@ -241,6 +244,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); MarqueeOptionsMenu.Instance.fadeOut(true); document.removeEventListener("pointerdown", hideMarquee); + document.removeEventListener("wheel", hideMarquee); }; if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100)) { MarqueeOptionsMenu.Instance.createCollection = this.collection; @@ -250,7 +254,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee; MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee; MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY); + MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; document.addEventListener("pointerdown", hideMarquee); + document.addEventListener("wheel", hideMarquee); } else { this.hideMarquee(); } @@ -349,7 +355,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.hideMarquee(); } - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, isBackground?: boolean) => { + getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, _isBackground?: boolean) => { const newCollection = creator ? creator(selected, { title: "nested stack", }) : ((doc: Doc) => { Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = "nested freeform"; @@ -357,8 +363,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.system = undefined; - newCollection.isBackground = isBackground; - newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined; + newCollection._isBackground = _isBackground; + newCollection.backgroundColor = this.props.isAnnotationOverlay ? "#00000015" : _isBackground ? "cyan" : undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection.x = this.Bounds.left; @@ -381,6 +387,38 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } @undoBatch @action + pinWithView = (e: KeyboardEvent | React.PointerEvent | undefined) => { + const doc = this.props.Document; + const bounds = this.Bounds; + const selected = this.marqueeSelect(false); + const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + if (curPres) { + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.presZoomButton = true; + pinDoc.context = curPres; + Doc.AddDocToList(curPres, "data", pinDoc); + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + if (!DocumentManager.Instance.getDocumentView(curPres)) { + CollectionDockingView.AddSplit(curPres, "right"); + } + if (e instanceof KeyboardEvent ? e.key === "c" : true) { + const x = this.Bounds.left + this.Bounds.width / 2; + const y = this.Bounds.top + this.Bounds.height / 2; + const panelWidth: number = this.props.PanelWidth(); + const panelHeight: number = this.props.PanelHeight(); + const scale = Math.min(Number(panelWidth) / this.Bounds.width, Number(panelHeight) / this.Bounds.height); + pinDoc.presPinView = true; + pinDoc.presPinViewX = x; + pinDoc.presPinViewY = y; + pinDoc.presPinViewScale = scale; + } + } + MarqueeOptionsMenu.Instance.fadeOut(true); + this.hideMarquee(); + } + + @undoBatch @action collection = (e: KeyboardEvent | React.PointerEvent | undefined) => { const bounds = this.Bounds; const selected = this.marqueeSelect(false); @@ -492,7 +530,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque d.page = -1; return d; }); - const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); + const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" }); const portal = Doc.MakeAlias(summary); Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected); Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations"); @@ -644,7 +682,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque marqueeSelect(selectBackgrounds: boolean = true) { const selRect = this.Bounds; const selection: Doc[] = []; - this.props.activeDocuments().filter(doc => !doc.isBackground && !doc.z).map(doc => { + this.props.activeDocuments().filter(doc => !doc._isBackground && !doc.z).map(doc => { const layoutDoc = Doc.Layout(doc); const x = NumCast(doc.x); const y = NumCast(doc.y); @@ -719,14 +757,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque </div>; } else { - //subtracted 250 for offset var str: string = ""; for (var i = 0; i < this._pointsX.length; i++) { - var x = 0; - x = this._pointsX[i] - 250; - str += x.toString(); + const pt = this.props.getContainerTransform().transformPoint(this._pointsX[i], this._pointsY[i]); + str += pt[0].toString(); str += ","; - str += this._pointsY[i].toString(); + str += pt[1].toString(); str += (" "); } @@ -747,7 +783,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque render() { return <div className="marqueeView" - style={{ overflow: StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} + style={{ overflow: !this.props.ContainingCollectionView && this.props.annotationsKey ? "visible" : StrCast(this.props.Document._overflow), cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" }} onDragOver={e => e.preventDefault()} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this._visible ? this.marqueeDiv : null} diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 4d8473be9..60ec02f47 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -134,17 +134,6 @@ } -// .documentDecorations-container .documentDecorations-resizer { -// pointer-events: none; -// } - -// #documentDecorations-bottomRightResizer, -// #documentDecorations-bottomLeftResizer, -// #documentDecorations-topRightResizer, -// #documentDecorations-topLeftResizer { -// visibility: collapse; -// } - /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e6ac7021a..4e279c659 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -94,8 +94,8 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ unflexedPosition(index: number): Omit<Layout, "i"> { return { - x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW, - y: Math.floor(index / Math.floor(this.numCols / this.defaultH)) * this.defaultH, + x: (index % (Math.floor(this.numCols / this.defaultW) || 1)) * this.defaultW, + y: Math.floor(index / (Math.floor(this.numCols / this.defaultH) || 1)) * this.defaultH, w: this.defaultW, h: this.defaultH, static: true @@ -304,7 +304,7 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { (e: PointerEvent, doubleTap?: boolean) => { if (doubleTap) { undoBatch(action(() => { - const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 }); + const text = Docs.Create.TextDocument("", { _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, _width: 150, _height: 50 }); FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 21d283547..0afcab5a3 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -234,6 +234,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index d02088a6c..53825eece 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -234,6 +234,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.docFilters} + searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss index 7eb07ff55..d0c90edcb 100644 --- a/src/client/views/globalCssVariables.scss +++ b/src/client/views/globalCssVariables.scss @@ -36,6 +36,12 @@ $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should $COLLECTION_BORDER_WIDTH: 1; $MINIMIZED_ICON_SIZE:25; $MAX_ROW_HEIGHT: 44px; +$DFLT_IMAGE_NATIVE_DIM: 900px; +$MENU_PANEL_WIDTH: 60px; +:root { + --flyoutHandleWidth: 28px; + --menuPanelWidth: 60px; +} :export { contextMenuZindex: $contextMenu-zindex; @@ -45,4 +51,6 @@ $MAX_ROW_HEIGHT: 44px; SEARCH_THUMBNAIL_SIZE: $search-thumnail-size; ANTIMODEMENU_HEIGHT: $antimodemenu-height; SEARCH_PANEL_HEIGHT: $searchpanel-height; + DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM; + MENU_PANEL_WIDTH: $MENU_PANEL_WIDTH; }
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts index fd1f60f13..fb3a327d0 100644 --- a/src/client/views/globalCssVariables.scss.d.ts +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -7,6 +7,8 @@ interface IGlobalScss { SEARCH_THUMBNAIL_SIZE: string; ANTIMODEMENU_HEIGHT: string; SEARCH_PANEL_HEIGHT: string; + DFLT_IMAGE_NATIVE_DIM: string; + MENU_PANEL_WIDTH: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx index ed64bde32..d6c3702d1 100644 --- a/src/client/views/linking/LinkEditor.tsx +++ b/src/client/views/linking/LinkEditor.tsx @@ -1,5 +1,3 @@ -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, observable } from "mobx"; @@ -12,9 +10,6 @@ import { undoBatch } from "../../util/UndoManager"; import './LinkEditor.scss'; import React = require("react"); -library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus); - - interface GroupTypesDropdownProps { groupType: string; setGroupType: (group: string) => void; @@ -369,7 +364,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-followingDropdown-dropdown"> <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}> - {StrCast(this.props.linkDoc.followLinkLocation, "Default")} + {StrCast(this.props.linkDoc.followLinkLocation, "default")} <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? "chevron-up" : "chevron-down"} size={"lg"} /> @@ -377,17 +372,37 @@ export class LinkEditor extends React.Component<LinkEditorProps> { <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? "" : "none" }}> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("Default")}> + onPointerDown={() => this.changeFollowBehavior("default")}> Default </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("onRight")}> - Always open in a new pane + onPointerDown={() => this.changeFollowBehavior("add:left")}> + Always open in new left pane + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("add:right")}> + Always open in new right pane + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("replace:right")}> + Always replace right tab </div> <div className="linkEditor-followingDropdown-option" - onPointerDown={() => this.changeFollowBehavior("inTab")}> + onPointerDown={() => this.changeFollowBehavior("replace:left")}> + Always replace left tab + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("fullScreen")}> + Always open full screen + </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("add")}> Always open in a new tab </div> + <div className="linkEditor-followingDropdown-option" + onPointerDown={() => this.changeFollowBehavior("replace")}> + Replace Tab + </div> {this.props.linkDoc.linksToAnnotation ? <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior("openExternal")}> diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 4dc25031d..0e03b46db 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -4,8 +4,6 @@ width: auto; height: auto; position: absolute; - top: 0; - left: 0; z-index: 999; .linkMenu-list { diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 8ecde959f..f5a1ae8e7 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -1,18 +1,14 @@ -import { action, observable, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { DocumentView } from "../nodes/DocumentView"; -import { LinkEditor } from "./LinkEditor"; -import './LinkMenu.scss'; -import React = require("react"); import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; -import { LinkMenuGroup } from "./LinkMenuGroup"; -import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import { library } from "@fortawesome/fontawesome-svg-core"; import { DocumentLinksButton } from "../nodes/DocumentLinksButton"; +import { DocumentView } from "../nodes/DocumentView"; import { LinkDocPreview } from "../nodes/LinkDocPreview"; - -library.add(faTrash); +import { LinkEditor } from "./LinkEditor"; +import './LinkMenu.scss'; +import { LinkMenuGroup } from "./LinkMenuGroup"; +import React = require("react"); interface Props { docView: DocumentView; @@ -93,12 +89,11 @@ export class LinkMenu extends React.Component<Props> { } render() { - console.log("computed", this.position.x, this.position.b); const sourceDoc = this.props.docView.props.Document; const groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc); - return <div className="linkMenu" ref={this._linkMenuRef} > + return <div className="linkMenu" style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.b + 15, bottom: this.props.docView.topMost ? 20 : undefined }} ref={this._linkMenuRef} > {!this._editingLink ? - <div className="linkMenu-list" style={{ left: this.position.x, top: this.position.b + 15 }}> + <div className="linkMenu-list" > {this.renderAllGroups(groups)} </div> : <div className="linkMenu-listEditor" style={{ left: this.position.x, top: this.position.b + 15 }}> diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 2ae87ac13..8e09052a3 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -66,7 +66,8 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { } render() { - const groupItems = this.props.group.map(linkDoc => { + const set = new Set<Doc>(this.props.group); + const groupItems = Array.from(set.keys()).map(linkDoc => { const destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc) || LinkManager.Instance.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); if (destination && this.props.sourceDoc) { diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 21c666a4d..17549e3cf 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -1,27 +1,23 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes, faPencilAlt, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; import { action, observable, runInAction } from 'mobx'; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { Cast, StrCast } from '../../../fields/Types'; +import { WebField } from '../../../fields/URLField'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; +import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; +import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; -import './LinkMenuItem.scss'; -import React = require("react"); -import { DocumentManager } from '../../util/DocumentManager'; -import { setupMoveUpEvents, emptyFunction, Utils, simulateMouseClick } from '../../../Utils'; -import { DocumentView } from '../nodes/DocumentView'; import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; +import { DocumentView } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; -import { Hypothesis } from '../../util/HypothesisUtils'; -import { Id } from '../../../fields/FieldSymbols'; -import { Tooltip } from '@material-ui/core'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { undoBatch } from '../../util/UndoManager'; -import { WebField } from '../../../fields/URLField'; -library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp, faPencilAlt, faEyeSlash); +import './LinkMenuItem.scss'; +import React = require("react"); interface LinkMenuItemProps { @@ -161,7 +157,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return; } - if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "Default") { + if (linkDoc.followLinkLocation && linkDoc.followLinkLocation !== "default") { const annotationOn = this.props.destinationDoc.annotationOn as Doc; this.props.addDocTab(annotationOn instanceof Doc ? annotationOn : this.props.destinationDoc, StrCast(linkDoc.followLinkLocation)); if (annotationOn) { @@ -171,7 +167,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { }); } } else { - DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false); + DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "add:right"), false); } linkDoc.linksToAnnotation && Hypothesis.scrollToAnnotation(StrCast(this.props.linkDoc.annotationId), this.props.destinationDoc); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 77e2df94c..2edb73cee 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -109,12 +109,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this._linkPlayDisposer?.(); this._scrubbingDisposer?.(); } + + @action componentDidMount() { if (!this.dataDoc.markerAmount) { this.dataDoc.markerAmount = 0; } - runInAction(() => this.audioState = this.path ? "paused" : undefined); + this.audioState = this.path ? "paused" : undefined; this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID, scrollLinkId => { if (scrollLinkId) { @@ -169,11 +171,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration); DocListCast(this.dataDoc.links).map(l => { const { la1, linkTime } = this.getLinkData(l); - if (linkTime > NumCast(this.layoutDoc.currentTimecode) && linkTime < htmlEle.currentTime) { + if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < htmlEle.currentTime) { Doc.linkFollowHighlight(la1); } }); - this.layoutDoc.currentTimecode = htmlEle.currentTime; + this.layoutDoc._currentTimecode = htmlEle.currentTime; } } @@ -223,7 +225,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (this._paused) { this._pausedTime += (new Date().getTime() - this._recordStart) / 1000; } else { - this.layoutDoc.currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; + this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000; } } } @@ -285,7 +287,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD // creates a text document for dictation onFile = (e: any) => { const newDoc = Docs.Create.TextDocument("", { - title: "", _chromeStatus: "disabled", + _showTitle: Doc.UserDoc().showTitle ? "title" : undefined, title: "", _chromeStatus: "disabled", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10, _width: NumCast(this.props.Document._width), _height: 2 * NumCast(this.props.Document._height) }); @@ -395,7 +397,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD this.changeMarker(this._currMarker, toTimeline(e.clientX - rect.x)); return false; }, - () => this._ele!.currentTime = this.layoutDoc.currentTimecode = toTimeline(e.clientX - rect.x), + () => this._ele!.currentTime = this.layoutDoc._currentTimecode = toTimeline(e.clientX - rect.x), emptyFunction); } @@ -463,7 +465,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD color={"darkblue"} height={this._waveHeight} barWidth={0.1} - // pos={this.layoutDoc.currentTimecode} need to correctly resize parent to make this work (not very necessary for function) + // pos={this.layoutDoc._currentTimecode} need to correctly resize parent to make this work (not very necessary for function) pos={this.audioDuration} duration={this.audioDuration} peaks={this._buckets.length === 100 ? this._buckets : undefined} @@ -544,7 +546,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD <div className="buttons" onClick={this._paused ? this.recordPlay : this.recordPause}> <FontAwesomeIcon style={{ width: "100%" }} icon={this._paused ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /> </div> - <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))}</div> + <div className="time">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> </div> : <button className={`audiobox-record${interactive}`} style={{ backgroundColor: "black" }}> @@ -562,7 +564,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD if (e.target !== this._audioRef.current) { const wasPaused = this.audioState === "paused"; - this._ele!.currentTime = this.layoutDoc.currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; + this._ele!.currentTime = this.layoutDoc._currentTimecode = (e.clientX - rect.x) / rect.width * this.audioDuration; wasPaused && this.pause(); } @@ -624,11 +626,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps, AudioD })} {this._visible ? this.selectionContainer : null} - <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc.currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> + <div className="audiobox-current" ref={this._audioRef} onClick={e => { e.stopPropagation(); e.preventDefault(); }} style={{ left: `${NumCast(this.layoutDoc._currentTimecode) / this.audioDuration * 100}%`, pointerEvents: "none" }} /> {this.audio} </div> <div className="current-time"> - {formatTime(Math.round(NumCast(this.layoutDoc.currentTimecode)))} + {formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))} </div> <div className="total-time"> {formatTime(Math.round(this.audioDuration))} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 52f6a66c8..16b2f7c2a 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -58,21 +58,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); } - @computed get renderScriptDim() { - if (this.Document.renderScript) { - const someView = Cast(this.props.Document.someView, Doc); - const minimap = Cast(this.props.Document.minimap, Doc); - if (someView instanceof Doc && minimap instanceof Doc) { - const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2; - const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView._viewScale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2; - const w = NumCast(someView._width) / NumCast(someView._viewScale) / NumCast(minimap.fitW) * NumCast(minimap.width); - const h = NumCast(someView._height) / NumCast(someView._viewScale) / NumCast(minimap.fitH) * NumCast(minimap.height); - return { x: x, y: y, width: w, height: h }; - } - } - return undefined; - } - public static getValues(doc: Doc, time: number) { const timecode = Math.round(time); return ({ @@ -123,11 +108,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF setTimeout(() => doc.dataTransition = "inherit", 1010); } - public static setupScroll(doc: Doc, timecode: number, scrollProgressivize: boolean = false) { + public static setupScroll(doc: Doc, timecode: number) { const scrollList = new List<number>(); scrollList[timecode] = NumCast(doc._scrollTop); doc["scroll-indexed"] = scrollList; - doc.activeFrame = ComputedField.MakeFunction("self.currentFrame"); + doc.activeFrame = ComputedField.MakeFunction("self._currentFrame"); doc._scrollTop = ComputedField.MakeInterpolated("scroll", "activeFrame"); } @@ -165,7 +150,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } - public static setupZoom(doc: Doc, targDoc: Doc, zoomProgressivize: boolean = false) { + public static setupZoom(doc: Doc, targDoc: Doc) { const width = new List<number>(); const height = new List<number>(); const top = new List<number>(); @@ -180,32 +165,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF doc["viewfinder-left-indexed"] = left; } - public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) { - docs.forEach((doc, i) => { - if (doc.appearFrame === undefined) doc.appearFrame = i; - const curTimecode = progressivize ? i : timecode; - const xlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const ylist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const wlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const hlist = new List<number>(numberRange(timecode + 1).map(i => undefined) as any as number[]); - const olist = new List<number>(numberRange(timecode + 1).map(t => progressivize && t < (doc.appearFrame ? doc.appearFrame : i) ? 0 : 1)); - const oarray = olist; - oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); - oarray.fill(1, NumCast(doc.appearFrame), timecode); - // oarray.fill(0, 0, NumCast(doc.appearFrame) - 1); - // oarray.fill(1, NumCast(doc.appearFrame), timecode);\ + public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { + docs.forEach(doc => { + if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; + const curTimecode = currTimecode; + const xlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const ylist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const wlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const hlist = new List<number>(numberRange(currTimecode + 1).map(i => undefined) as any as number[]); + const olist = new List<number>(numberRange(currTimecode + 1).map(t => !doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)); wlist[curTimecode] = NumCast(doc._width); hlist[curTimecode] = NumCast(doc._height); xlist[curTimecode] = NumCast(doc.x); ylist[curTimecode] = NumCast(doc.y); - doc.xArray = xlist; - doc.yArray = ylist; doc["x-indexed"] = xlist; doc["y-indexed"] = ylist; doc["w-indexed"] = wlist; doc["h-indexed"] = hlist; - doc["opacity-indexed"] = oarray; - doc.activeFrame = ComputedField.MakeFunction("self.context?.currentFrame||0"); + doc["opacity-indexed"] = olist; + doc.activeFrame = ComputedField.MakeFunction("self.context?._currentFrame||0"); doc._height = ComputedField.MakeInterpolated("h", "activeFrame"); doc._width = ComputedField.MakeInterpolated("w", "activeFrame"); doc.x = ComputedField.MakeInterpolated("x", "activeFrame"); @@ -274,8 +252,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF boxShadow: this.Opacity === 0 ? undefined : // if it's not visible, then no shadow this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow - this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document, this.props.renderDepth)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent - this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big + this.props.backgroundHalo?.() && this.props.Document.type !== DocumentType.INK ? (`${this.props.backgroundColor?.(this.props.Document, this.props.renderDepth)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc._isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent + this.layoutDoc._isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big StrCast(this.layoutDoc.boxShadow, ""), borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding), outline: this.Highlight ? "orange solid 2px" : "", @@ -286,7 +264,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF zIndex: this.ZInd, mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any, display: this.ZInd === -99 ? "none" : undefined, - pointerEvents: this.props.Document.isBackground || this.Opacity === 0 || this.props.Document.type === DocumentType.INK || this.props.Document.isInkMask ? "none" : this.props.pointerEvents ? "all" : undefined + pointerEvents: this.props.Document._isBackground || this.Opacity === 0 || this.props.Document.type === DocumentType.INK || this.props.Document.isInkMask ? "none" : this.props.pointerEvents ? "all" : undefined }} > {Doc.UserDoc().renderStyle !== "comic" ? (null) : diff --git a/src/client/views/nodes/DocHolderBox.tsx b/src/client/views/nodes/DocHolderBox.tsx index 88eb48f51..b3b7cc4f3 100644 --- a/src/client/views/nodes/DocHolderBox.tsx +++ b/src/client/views/nodes/DocHolderBox.tsx @@ -120,6 +120,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} @@ -149,6 +150,7 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do DataDoc={undefined} LibraryPath={emptyPath} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected ContainingCollectionDoc={undefined} fitToBox={true} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 2408b3906..1b2070c0f 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -18,14 +18,14 @@ import { DocHolderBox } from "./DocHolderBox"; import { DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import { FontIconBox } from "./FontIconBox"; -import { MenuIconBox } from "./MenuIconBox"; import { FieldView, FieldViewProps } from "./FieldView"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; +import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox"; import { ImageBox } from "./ImageBox"; import { KeyValueBox } from "./KeyValueBox"; import { PDFBox } from "./PDFBox"; import { PresBox } from "./PresBox"; import { SearchBox } from "../search/SearchBox"; +import { FilterBox } from "./FilterBox"; import { ColorBox } from "./ColorBox"; import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo"; import { LinkAnchorBox } from "./LinkAnchorBox"; @@ -93,7 +93,7 @@ export class HTMLtag extends React.Component<HTMLtagProps> { } @observer -export class DocumentContentsView extends React.Component<DocumentViewProps & { +export class DocumentContentsView extends React.Component<DocumentViewProps & FormattedTextBoxProps & { isSelected: (outsideReaction: boolean) => boolean, select: (ctrl: boolean) => void, layoutKey: string, @@ -190,9 +190,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { blacklistedAttrs={[]} renderInWrapper={false} components={{ - FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, MenuIconBox, LabelBox, SliderBox, FieldView, + FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox, - PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, + PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox, ScreenshotBox, HTMLtag, ComparisonBox }} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index cf8645e4c..ab6cae0ad 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -25,7 +25,7 @@ export const Flyout = higflyout.default; interface DocumentLinksButtonProps { View: DocumentView; - Offset?: number[]; + Offset?: (number | undefined)[]; AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; @@ -36,6 +36,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp private _linkButton = React.createRef<HTMLDivElement>(); @observable public static StartLink: Doc | undefined; + @observable public static StartLinkView: DocumentView | undefined; @observable public static AnnotationId: string | undefined; @observable public static AnnotationUri: string | undefined; @@ -79,8 +80,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp //action(() => Doc.BrushDoc(this.props.View.Document)); if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } else { DocumentLinksButton.StartLink = this.props.View.props.Document; + DocumentLinksButton.StartLinkView = this.props.View; } } else if (!this.props.InMenu) { DocumentLinksButton.EditLink = this.props.View; @@ -95,8 +98,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp DocumentLinksButton.AnnotationUri = undefined; if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } else { DocumentLinksButton.StartLink = this.props.View.props.Document; + DocumentLinksButton.StartLinkView = this.props.View; } //action(() => Doc.BrushDoc(this.props.View.Document)); @@ -110,6 +115,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp if (doubleTap && !this.props.StartLink) { if (DocumentLinksButton.StartLink === this.props.View.props.Document) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) { const sourceDoc = DocumentLinksButton.StartLink; @@ -129,9 +135,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp LinkDescriptionPopup.popupY = e.screenY - 100; LinkDescriptionPopup.descriptionPopup = true; - LinkDescriptionPopup.popupX = e.screenX; - LinkDescriptionPopup.popupY = e.screenY - 100; - LinkDescriptionPopup.descriptionPopup = true; + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); } @@ -144,6 +156,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp public static finishLinkClick = undoBatch(action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView,) => { if (startLink === endLink) { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; DocumentLinksButton.AnnotationUri = undefined; //!this.props.StartLink @@ -151,8 +164,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "long drag"); // this notifies any of the subviews that a document is made so that they can make finer-grained hyperlinks (). see note above in onLInkButtonMoved if (endLinkView) { - startLink._link = endLinkView._link = linkDoc; - setTimeout(action(() => startLink._link = endLinkView._link = undefined), 0); + endLinkView._link = linkDoc; + DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView._link = linkDoc); + setTimeout(action(() => { + DocumentLinksButton.StartLinkView && (DocumentLinksButton.StartLinkView._link = undefined); + endLinkView._link = undefined; + }), 0); } LinkManager.currentLink = linkDoc; @@ -176,6 +193,17 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp LinkDescriptionPopup.popupY = screenY - 100; LinkDescriptionPopup.descriptionPopup = true; } + + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } + setTimeout(action(() => { TaskCompletionBox.taskCompleted = false; }), 2500); } } @@ -186,12 +214,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @action clearLinks() { DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; } @computed get linkButton() { TraceMobx(); - const links = this.props.links; + const links = new Set<Doc>(this.props.links); const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link"; const buttonTitle = "Tap to view links"; @@ -213,7 +242,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp id={"link-icon"} src={`/assets/${"link.png"}`} />; - const linkButton = <div ref={this._linkButton} style={{ minWidth: 20, minHeight: 20, position: "absolute", left: this.props.Offset?.[0] }}> + const linkButton = <div className="documentLinksButton-cont" ref={this._linkButton} style={{ + minWidth: 20, minHeight: 20, position: "absolute", + left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] + }}> <div className={"documentLinksButton"} style={{ backgroundColor: this.props.InMenu ? "" : "#add8e6", color: this.props.InMenu ? "white" : "black", @@ -233,7 +265,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp <FontAwesomeIcon className="documentdecorations-icon" icon="hand-paper" size="sm" /> : links.length} */} {this.props.InMenu ? this.props.StartLink ? <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" /> : - link : links.length} + link : Array.from(links).length} </div> {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? @@ -245,7 +277,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp border: DocumentLinksButton.StartLink ? "" : "none" }} onPointerDown={DocumentLinksButton.StartLink ? this.completeLink : emptyFunction} - onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.screenX, e.screenY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null) + onClick={e => DocumentLinksButton.StartLink ? DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View) : emptyFunction} /> : (null) } { DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.InMenu && this.props.StartLink ? <div className={"documentLinksButton-startLink"} @@ -255,7 +287,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } </div >; - return (!links.length) && !this.props.AlwaysOn ? (null) : + return (!Array.from(links).length) && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> {linkButton} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index e6b8928d4..2dd3bba91 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -133,6 +133,8 @@ bottom: 0; width: 100%; transform-origin: bottom left; + opacity: 0.1; + transition: opacity 0.5s; } } @@ -144,4 +146,9 @@ display:inline-block; } } + > .documentView-styleWrapper { + > .documentView-captionWrapper { + opacity: 1; + } + } }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index d92dc0ec2..f6360fc87 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -21,7 +21,7 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { LinkManager } from '../../util/LinkManager'; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from "../../util/SelectionManager"; -import SharingManager from '../../util/SharingManager'; +import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from "../../util/Transform"; import { undoBatch, UndoManager } from "../../util/UndoManager"; @@ -30,6 +30,7 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from "../DocComponent"; import { EditableView } from '../EditableView'; +import { InkStrokeProperties } from '../InkStrokeProperties'; import { DocumentContentsView } from "./DocumentContentsView"; import { DocumentLinksButton } from './DocumentLinksButton'; import "./DocumentView.scss"; @@ -38,6 +39,7 @@ import { LinkDescriptionPopup } from './LinkDescriptionPopup'; import { RadialMenu } from './RadialMenu'; import { TaskCompletionBox } from './TaskCompletedBox'; import React = require("react"); +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; export type DocFocusFunc = () => boolean; @@ -45,11 +47,13 @@ export interface DocumentViewProps { ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; docFilters: () => string[]; + searchFilterDocs: () => Doc[]; FreezeDimensions?: boolean; NativeWidth: () => number; NativeHeight: () => number; Document: Doc; DataDoc?: Doc; + getView?: (view: DocumentView) => any; LayoutTemplateString?: string; LayoutTemplate?: () => Opt<Doc>; LibraryPath: Doc[]; @@ -69,7 +73,7 @@ export interface DocumentViewProps { removeDocument?: (doc: Doc | Doc[]) => boolean; moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - setupDragLines?: () => void; + setupDragLines?: (snapToDraggedDoc: boolean) => void; renderDepth: number; ContentScaling: () => number; PanelWidth: () => number; @@ -104,17 +108,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu private _lastTap: number = 0; private _doubleTap = false; private _mainCont = React.createRef<HTMLDivElement>(); - private _dropDisposer?: DragManager.DragDropDisposer; - private _showKPQuery: boolean = false; - private _queries: string = ""; private _titleRef = React.createRef<EditableView>(); + private _dropDisposer?: DragManager.DragDropDisposer; private _gestureEventDisposer?: GestureUtils.GestureEventDisposer; private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive public get ContentDiv() { return this._mainCont.current; } - private get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); } @computed get topMost() { return this.props.renderDepth === 0; } @computed get freezeDimensions() { return this.props.FreezeDimensions; } @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); } @@ -128,6 +130,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onClickFunc = () => this.onClickHandler; onDoubleClickFunc = () => this.onDoubleClickHandler; + constructor(props: any) { + super(props); + props.getView?.(this); + } + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => { this.removeMoveListeners(); this.removeEndListeners(); @@ -174,10 +181,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); - // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); @@ -191,7 +198,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._mainCont.current && (this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this))); // this._mainCont.current && (this.holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this))); - if (!this.props.dontRegisterView) { + if (!BoolCast(this.rootDoc.dontRegisterView, this.props.dontRegisterView)) { DocumentManager.Instance.DocumentViews.push(this); } } @@ -285,13 +292,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } + _timeout: NodeJS.Timeout | undefined; + onClick = action((e: React.MouseEvent | React.PointerEvent) => { if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { let stopPropagate = true; let preventDefault = true; - !this.props.Document.isBackground && this.props.bringToFront(this.props.Document); - if (this._doubleTap && this.props.renderDepth) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + !this.props.Document._isBackground && this.props.bringToFront(this.props.Document); + if (this._doubleTap && ((this.props.renderDepth && this.props.Document.type !== DocumentType.FONTICON) || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = undefined; + } if (!(e.nativeEvent as any).formattedHandled) { if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself const func = () => this.onDoubleClickHandler.script.run({ @@ -301,40 +314,53 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu shiftKey: e.shiftKey }, console.log); func(); - } else { - UndoManager.RunInBatch(() => { - let fullScreenDoc = this.props.Document; - if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { - fullScreenDoc = Doc.MakeAlias(this.props.Document); - fullScreenDoc.layoutKey = "layout_fullScreen"; - } - this.props.addDocTab(fullScreenDoc, "inTab"); - }, "double tap"); - SelectionManager.DeselectAll(); + } else if (!Doc.IsSystem(this.props.Document)) { + if (this.props.Document.type === DocumentType.INK) { + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true); + } else { + UndoManager.RunInBatch(() => { + let fullScreenDoc = this.props.Document; + if (StrCast(this.props.Document.layoutKey) !== "layout_fullScreen" && this.props.Document.layout_fullScreen) { + fullScreenDoc = Doc.MakeAlias(this.props.Document); + fullScreenDoc.layoutKey = "layout_fullScreen"; + } + this.props.addDocTab(fullScreenDoc, "add"); + }, "double tap"); + SelectionManager.DeselectAll(); + } Doc.UnBrushDoc(this.props.Document); } } } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + const shiftKey = e.shiftKey; const func = () => this.onClickHandler.script.run({ this: this.layoutDoc, self: this.rootDoc, scriptContext: this.props.scriptContext, thisContainer: this.props.ContainingCollectionDoc, documentView: this, - shiftKey: e.shiftKey + shiftKey }, console.log); - if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc)) { - UndoManager.RunInBatch(func, "on click"); - } else func(); + const clickFunc = () => { + if (!Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-undo"] as Doc) && + !Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()["dockedBtn-redo"] as Doc) && + !this.onClickHandler.script.originalScript.includes("selectMainMenu")) { + UndoManager.RunInBatch(func, "on click"); + } else func(); + }; + if (this.onDoubleClickHandler) { + this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 500); + } else clickFunc(); } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself - this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "onRight"); + this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); } else if (this.allLinks && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { this.allLinks.length && this.followLinkClick(e.altKey, e.ctrlKey, e.shiftKey); } else { if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template } else { - SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); + this.select(e.ctrlKey || e.shiftKey); + //SelectionManager.SelectDoc(this, e.ctrlKey || e.shiftKey); } preventDefault = false; } @@ -564,8 +590,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action deleteClicked = (): void => { - if (Doc.UserDoc().activeWorkspace === this.props.Document) { - alert("Can't delete the active workspace"); + if (CurrentUserUtils.ActiveDashboard === this.props.Document) { + alert("Can't delete the active dashboard"); } else { const selected = SelectionManager.SelectedDocuments().slice(); SelectionManager.DeselectAll(); @@ -601,8 +627,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (this.props.Document === Doc.UserDoc().activeWorkspace) { - alert("linking to document tabs not yet supported. Drop link on document content."); + if (this.props.Document === CurrentUserUtils.ActiveDashboard) { + if ((e.target as any)?.closest?.("*.lm_content")) { + alert("You can't perform this move most likely because you don't have permission to modify the destination."); + } + else alert("linking to document tabs not yet supported. Drop link on document content."); return; } const makeLink = action((linkDoc: Doc) => { @@ -617,15 +646,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu LinkDescriptionPopup.popupY = de.y; LinkDescriptionPopup.descriptionPopup = true; + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); }); if (de.complete.annoDragData) { /// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner e.stopPropagation(); - de.complete.annoDragData.linkedToDoc = true; - - const linkDoc = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); - linkDoc && makeLink(linkDoc); + de.complete.annoDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link"); + de.complete.annoDragData.linkDocument && makeLink(de.complete.annoDragData.linkDocument); } if (de.complete.linkDragData) { e.stopPropagation(); @@ -666,12 +703,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @undoBatch @action toggleBackground = () => { - this.Document.isBackground = (this.Document.isBackground ? undefined : true); - this.Document._overflow = this.Document.isBackground ? "visible" : undefined; - if (this.Document.isBackground) { + this.Document._isBackground = (this.Document._isBackground ? undefined : true); + this.Document._overflow = this.Document._isBackground ? "visible" : undefined; + if (this.Document._isBackground) { this.props.bringToFront(this.props.Document, true); - this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = this.Document[WidthSym](); - this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeHeight"] = this.Document[HeightSym](); + const wid = this.Document[WidthSym](); // change the nativewidth and height if the background is to be a collection that aggregates stuff that is added to it. + const hgt = this.Document[HeightSym](); + this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeWidth"] = wid; + this.props.Document[DataSym][Doc.LayoutFieldKey(this.Document) + "-nativeHeight"] = hgt; } } @@ -684,7 +723,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @action - onContextMenu = async (e: React.MouseEvent | Touch): Promise<void> => { + onContextMenu = (e: React.MouseEvent | Touch) => { // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 if (!(e instanceof Touch)) { if (e.button === 0 && !e.ctrlKey) { @@ -703,38 +742,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } const cm = ContextMenu.Instance; - if (!cm) return; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); Cast(this.props.Document.contextMenuLabels, listSpec("string"), []).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); this.props.contextMenuItems?.().forEach(item => - cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); + item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, self: this.rootDoc }), icon: "sticky-note" })); const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); const appearance = cm.findByDescription("UI Controls..."); const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; - templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" }); + templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); //DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); + optionItems.push({ description: "Bring to Front", event: () => this.props.bringToFront(this.rootDoc, false), icon: "expand-arrows-alt" }); + optionItems.push({ description: "Send to Back", event: () => this.props.bringToFront(this.rootDoc, true), icon: "expand-arrows-alt" }); + !this.props.treeViewDoc && this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform && optionItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" }); !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); - onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("onRight", false, false), icon: "link" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); - !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`), icon: "concierge-bell" }); + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); + !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); + onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + } const funcs: ContextMenuProps[] = []; if (this.layoutDoc.onDragStart) { @@ -760,18 +802,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); - Doc.AreProtosEqual(this.props.Document, Doc.UserDoc()) && moreItems.push({ description: "Toggle Always Show Link End", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" }); + Doc.AreProtosEqual(this.props.Document, Cast(Doc.UserDoc().myUserDoc, Doc, null)) && moreItems.push({ description: "Toggle Alternate Button Bar", event: () => Doc.UserDoc()["documentLinksButton-hideEnd"] = !Doc.UserDoc()["documentLinksButton-hideEnd"], icon: "eye" }); } - moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + const collectionAcl = GetEffectiveAcl(this.props.ContainingCollectionDoc?.[DataSym]); + if ((collectionAcl === AclAdmin || collectionAcl === AclEdit) && this.props.removeDocument) { + moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + } !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); cm.moveAfter(cm.findByDescription("More...")!, cm.findByDescription("OnClick...")!); const help = cm.findByDescription("Help..."); const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "onRight"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); @@ -811,13 +856,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; } childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling()); - @computed.struct get linkOffset() { return [-15, 0]; } + @computed.struct get linkOffset() { return this.topMost ? [0, undefined, undefined, 10] : [-15, undefined, undefined, undefined]; } @computed get contents() { const pos = this.props.relative ? "relative " : "absolute"; TraceMobx(); return (<div className="documentView-contentsView" style={{ borderRadius: "inherit", width: "100%", height: "100%" }}> <DocumentContentsView key={1} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={this.props.ContainingCollectionView} ContainingCollectionDoc={this.props.ContainingCollectionDoc} NativeWidth={this.NativeWidth} @@ -879,31 +925,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu anchorPanelWidth = () => this.props.PanelWidth() || 1; anchorPanelHeight = () => this.props.PanelHeight() || 1; - @computed.struct get directLinks() { return LinkManager.Instance.getAllDirectLinks(this.Document); } + @computed.struct get directLinks() { return LinkManager.Instance.getAllDirectLinks(this.rootDoc); } @computed.struct get allLinks() { return DocListCast(this.Document.links); } @computed.struct get allAnchors() { TraceMobx(); if (this.props.LayoutTemplateString?.includes("LinkAnchorBox")) return null; - return (this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots + if ((this.props.treeViewDoc && this.props.LayoutTemplateString) || // render nothing for: tree view anchor dots this.layoutDoc.presBox || // presentationbox nodes this.rootDoc.type === DocumentType.LINK || - this.props.dontRegisterView ? (null) : // view that are not registered - DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink).map((d, i) => - <div className="documentView-anchorCont" key={i + 1}> - <DocumentView {...this.props} - Document={d} - ContainingCollectionView={this.props.ContainingCollectionView} - ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox - PanelWidth={this.anchorPanelWidth} - PanelHeight={this.anchorPanelHeight} - ContentScaling={returnOne} - dontRegisterView={false} - forcedBackgroundColor={returnTransparent} - removeDocument={this.hideLinkAnchor} - pointerEvents={false} - LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} /> - </div >); + this.props.dontRegisterView) {// view that are not registered + return (null); + } + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden && this.isNonTemporalLink(d)); + return filtered.map((d, i) => + <div className="documentView-anchorCont" key={i + 1}> + <DocumentView {...this.props} + Document={d} + ContainingCollectionView={this.props.ContainingCollectionView} + ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox + PanelWidth={this.anchorPanelWidth} + PanelHeight={this.anchorPanelHeight} + ContentScaling={returnOne} + dontRegisterView={false} + forcedBackgroundColor={returnTransparent} + removeDocument={this.hideLinkAnchor} + pointerEvents={false} + LayoutTemplate={undefined} + LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)} /> + </div >); } @computed get innards() { TraceMobx(); @@ -925,6 +974,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const captionView = (!showCaption ? (null) : <div className="documentView-captionWrapper" style={{ backgroundColor: StrCast(this.layoutDoc["caption-backgroundColor"]), color: StrCast(this.layoutDoc["caption-color"]) }}> <DocumentContentsView {...OmitKeys(this.props, ['children']).omit} + yMargin={10} + xMargin={10} hideOnLeave={true} LayoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`} ContentScaling={returnOne} @@ -955,7 +1006,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } @computed get ignorePointerEvents() { return this.props.pointerEvents === false || - (this.Document.isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || + (this.Document._isBackground && !this.isSelected() && !SnappingManager.GetIsDragging()) || (this.Document.type === DocumentType.INK && Doc.GetSelectedTool() !== InkTool.None); } @undoBatch @@ -977,11 +1028,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); renderLock() { - return (this.Document.isBackground !== undefined || this.isSelected(false)) && + return (this.Document._isBackground !== undefined || this.isSelected(false)) && ((this.Document.type === DocumentType.COL && this.Document._viewType !== CollectionViewType.Pile) || this.Document.type === DocumentType.IMG) && this.props.renderDepth > 0 && !this.props.treeViewDoc ? <div className="documentView-lock" onClick={this.toggleBackground}> - <FontAwesomeIcon icon={this.Document.isBackground ? "unlock" : "lock"} style={{ color: this.Document.isBackground ? "red" : undefined }} size="lg" /> + <FontAwesomeIcon icon={this.Document._isBackground ? "unlock" : "lock"} style={{ color: this.Document._isBackground ? "red" : undefined }} size="lg" /> </div> : (null); } @@ -997,7 +1048,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document); const borderRounding = this.layoutDoc.borderRounding; const localScale = fullDegree; - const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? + const highlightColors = CurrentUserUtils.ActiveDashboard?.darkScheme ? ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"]; const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"]; diff --git a/src/client/views/nodes/FaceRectangles.tsx b/src/client/views/nodes/FaceRectangles.tsx index 92ca276cb..0d1e063af 100644 --- a/src/client/views/nodes/FaceRectangles.tsx +++ b/src/client/views/nodes/FaceRectangles.tsx @@ -17,7 +17,7 @@ export interface RectangleTemplate { } @observer -export default class FaceRectangles extends React.Component<FaceRectanglesProps> { +export class FaceRectangles extends React.Component<FaceRectanglesProps> { render() { const faces = DocListCast(this.props.document.faces); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 9d61ec6d1..fceeced36 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -29,6 +29,7 @@ export interface FieldViewProps { dropAction: dropActionType; backgroundHalo?: () => boolean; docFilters: () => string[]; + searchFilterDocs: () => Doc[]; isSelected: (outsideReaction?: boolean) => boolean; select: (isCtrlPressed: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; @@ -58,9 +59,6 @@ export interface FieldViewProps { ChromeHeight?: () => number; childLayoutTemplate?: () => Opt<Doc>; - highlighting?: string[]; - lines?: string[]; - doc?: Doc; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) height?: number; width?: number; @@ -133,7 +131,7 @@ export class FieldView extends React.Component<FieldViewProps> { // ); } else if (field instanceof List) { - return <div> {field.map(f => Field.toString(f)).join(", ")} </div>; + return <div> {field.length ? field.map(f => Field.toString(f)).join(", ") : "[]"} </div>; } // bcz: this belongs here, but it doesn't render well so taking it out for now else if (field instanceof WebField) { diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss new file mode 100644 index 000000000..b39793f01 --- /dev/null +++ b/src/client/views/nodes/FilterBox.scss @@ -0,0 +1,54 @@ + + +.filterBox-flyout { + width: 400px; + display: block; + text-align: left; + .filterBox-flyout-facet { + background-color: lightgray; + text-align: left; + display: inline-block; + position: relative; + width: 100%; + } +} +.filterBox-treeView { + display: flex; + flex-direction: column; + width: 200px; + height: 100%; + position: absolute; + right: 0; + top: 0; + border-left: solid 1px; + z-index: 1; + + .filterBox-addfacet { + display: inline-block; + width: 200px; + height: 30px; + background: darkGray; + text-align: left; + + .filterBox-addFacetButton { + display: flex; + margin: auto; + + .filterBox-span { + margin-right: 15px; + } + } + + >div, + >div>div { + width: 100%; + height: 100%; + } + } + + .filterBox-tree { + display: inline-block; + width: 100%; + height: calc(100% - 30px); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx new file mode 100644 index 000000000..790901a29 --- /dev/null +++ b/src/client/views/nodes/FilterBox.tsx @@ -0,0 +1,215 @@ +import React = require("react"); +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { computed } from "mobx"; +import { observer } from "mobx-react"; +import { DataSym, Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; +import { documentSchema } from "../../../fields/documentSchemas"; +import { List } from "../../../fields/List"; +import { RichTextField } from "../../../fields/RichTextField"; +import { listSpec, makeInterface } from "../../../fields/Schema"; +import { ComputedField, ScriptField } from "../../../fields/ScriptField"; +import { Cast } from "../../../fields/Types"; +import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionTreeView } from "../collections/CollectionTreeView"; +import { ViewBoxBaseComponent } from "../DocComponent"; +import { SearchBox } from "../search/SearchBox"; +import { FieldView, FieldViewProps } from './FieldView'; +import './FilterBox.scss'; +import { Scripting } from "../../util/Scripting"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +type FilterBoxDocument = makeInterface<[typeof documentSchema]>; +const FilterBoxDocument = makeInterface(documentSchema); + +@observer +export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDocument>(FilterBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); } + + @computed get allDocs() { + const allDocs = new Set<Doc>(); + if (CollectionDockingView.Instance) { + const activeTabs = DocListCast(CollectionDockingView.Instance.props.Document.data); + SearchBox.foreachRecursiveDoc(activeTabs, (doc: Doc) => allDocs.add(doc)); + setTimeout(() => CollectionDockingView.Instance.props.Document.allDocuments = new List<Doc>(Array.from(allDocs))); + } + return allDocs; + } + + @computed get _allFacets() { + const noviceReqFields = ["author", "tags", "text", "type"]; + const noviceLayoutFields: string[] = [];//["_curPage"]; + const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; + + const keys = new Set<string>(noviceFields); + this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); + return Array.from(keys.keys()).filter(key => key[0] === "#" || key.indexOf("lastModified") !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith("_") && !key.startsWith("ACL")) || noviceFields.includes(key)).sort(); + } + /** + * Responds to clicking the check box in the flyout menu + */ + facetClick = (facetHeader: string) => { + const targetDoc = CollectionDockingView.Instance.props.Document; + const found = DocListCast(this.dataDoc[this.props.fieldKey]).findIndex(doc => doc.title === facetHeader); + if (found !== -1) { + (this.dataDoc[this.props.fieldKey] as List<Doc>).splice(found, 1); + const docFilter = Cast(targetDoc._docFilters, listSpec("string")); + if (docFilter) { + let index: number; + while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) { + docFilter.splice(index, 3); + } + } + const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec("string")); + if (docRangeFilters) { + let index: number; + while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) { + docRangeFilters.splice(index, 3); + } + } + } else { + const allCollectionDocs = DocListCast((targetDoc.data as any)[0].data); + var rtfields = 0; + const facetValues = Array.from(allCollectionDocs.reduce((set, child) => { + const field = child[facetHeader] as Field; + const fieldStr = Field.toString(field); + if (field instanceof RichTextField || (typeof (field) === "string" && fieldStr.split(" ").length > 2)) rtfields++; + return set.add(fieldStr); + }, new Set<string>())); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE; + facetValues.map(val => { + const num = Number(val); + if (Number.isNaN(num)) { + nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + let newFacet: Opt<Doc>; + if (facetHeader === "text" || rtfields / allCollectionDocs.length > 0.1) { + newFacet = Docs.Create.TextDocument("", { _width: 100, _height: 25, treeViewExpandedView: "layout", title: facetHeader, treeViewOpen: true, forceActive: true, ignoreClick: true }); + Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox + newFacet._textBoxPadding = 4; + const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; + newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); + } else if (facetHeader !== "tags" && nonNumbers / facetValues.length < .1) { + newFacet = Docs.Create.SliderDocument({ title: facetHeader, treeViewExpandedView: "layout", treeViewOpen: true }); + const newFacetField = Doc.LayoutFieldKey(newFacet); + const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader); + Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox + const extendedMinVal = minVal - Math.min(1, Math.abs(maxVal - minVal) * .05); + const extendedMaxVal = maxVal + Math.min(1, Math.abs(maxVal - minVal) * .05); + newFacet[newFacetField + "-min"] = ranged === undefined ? extendedMinVal : ranged[0]; + newFacet[newFacetField + "-max"] = ranged === undefined ? extendedMaxVal : ranged[1]; + Doc.GetProto(newFacet)[newFacetField + "-minThumb"] = extendedMinVal; + Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal; + const scriptText = `setDocFilterRange(this?.target, "${facetHeader}", range)`; + newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" }); + } else { + newFacet = new Doc(); + newFacet.sytem = true; + newFacet.title = facetHeader; + newFacet.treeViewOpen = true; + newFacet.type = DocumentType.COL; + const capturedVariables = { layoutDoc: targetDoc, dataDoc: (targetDoc.data as any)[0][DataSym] }; + newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, "${facetHeader}")`, {}, capturedVariables); + } + newFacet && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet); + } + } + filterBackground = () => "rgba(105, 105, 105, 0.432)"; + get ignoreFields() { return ["_docFilters", "_docRangeFilters"]; } // this makes the tree view collection ignore these filters (otherwise, the filters would filter themselves) + @computed get scriptField() { + const scriptText = "setDocFilter(this?.target, heading, this.title, checked)"; + const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + return script ? () => script : undefined; + } + + render() { + const facetCollection = this.props.Document.proto as Doc; + const flyout = <div className="filterBox-flyout" style={{ width: `100%`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}> + {this._allFacets.map(facet => <label className="filterBox-flyout-facet" key={`${facet}`} onClick={e => this.facetClick(facet)}> + <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey]).some(d => d.title === facet)} /> + <span className="checkmark" /> + {facet} + </label>)} + </div>; + + return this.props.dontRegisterView ? (null) : <div className="filterBox-treeView" style={{ width: "100%" }}> + <div className="filterBox-addFacet" style={{ width: "100%" }} onPointerDown={e => e.stopPropagation()}> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}> + <div className="filterBox-addFacetButton"> + <FontAwesomeIcon icon={"edit"} size={"lg"} /> + <span className="filterBox-span">Choose Facets</span> + </div> + </Flyout> + </div> + <div className="filterBox-tree" key="tree"> + <CollectionTreeView + PanelPosition={""} + Document={facetCollection} + DataDoc={Doc.GetProto(facetCollection)} + fieldKey={`${this.props.fieldKey}`} + CollectionView={undefined} + docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + ContainingCollectionView={this.props.ContainingCollectionView} + PanelWidth={this.props.PanelWidth} + PanelHeight={this.props.PanelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} + LibraryPath={emptyPath} + rootSelected={this.props.rootSelected} + renderDepth={1} + dropAction={this.props.dropAction} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + addDocTab={returnFalse} + pinToPres={returnFalse} + isSelected={returnFalse} + select={returnFalse} + bringToFront={emptyFunction} + active={this.props.active} + whenActiveChanged={returnFalse} + treeViewHideTitle={true} + ContentScaling={returnOne} + focus={returnFalse} + treeViewHideHeaderFields={true} + onCheckedClick={this.scriptField} + ignoreFields={this.ignoreFields} + annotationsKey={""} + dontRegisterView={true} + backgroundColor={this.filterBackground} + moveDocument={returnFalse} + removeDocument={returnFalse} + addDocument={returnFalse} /> + </div> + </div>; + } +} + +Scripting.addGlobal(function readFacetData(layoutDoc: Doc, facetHeader: string) { + const allCollectionDocs = DocListCast(CollectionDockingView.Instance?.props.Document.allDocuments); + const set = new Set<string>(); + if (facetHeader === "tags") allCollectionDocs.forEach(child => Field.toString(child[facetHeader] as Field).split(":").forEach(key => set.add(key))); + else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field))); + let facetValues = Array.from(set).filter(v => v); + + let nonNumbers = 0; + facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++); + const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => { + const doc = new Doc(); + doc.system = true; + doc.title = facetValue.toString(); + doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)", {}, { layoutDoc, facetHeader, facetValue }); + return doc; + }); + return new List<Doc>(facetValueDocSet); +});
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.scss b/src/client/views/nodes/FontIconBox.scss index 75bc90d7a..33ac85a0e 100644 --- a/src/client/views/nodes/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox.scss @@ -34,12 +34,14 @@ } } +.menuButton-circle, .menuButton-round { border-radius: 100%; background-color: black; + padding: 0; .fontIconBox-label { - margin-left: -10px; // button padding is 10px; + //margin-left: -10px; // button padding is 10px; bottom: 0; position: absolute; } @@ -52,7 +54,6 @@ .menuButton-square { padding-top: 3px; padding-bottom: 3px; - padding-left: 5px; .fontIconBox-label { border-radius: 0px; @@ -62,9 +63,11 @@ } .menuButton, +.menuButton-circle, .menuButton-round, .menuButton-square { - width: 100%; + margin-left: -5%; + width: 110%; height: 100%; pointer-events: all; touch-action: none; @@ -72,11 +75,7 @@ .menuButton-wrap { touch-action: none; border-radius: 8px; - - // &:hover { - // background: rgb(61, 61, 61); - // cursor: pointer; - // } + width: 100%; } .menuButton-icon-square { @@ -89,4 +88,13 @@ width: 95% !important; height: 95%; } +} +.menuButton-round { + width: 100%; + svg { + width: 50% !important; + height: 50%; + position: relative; + bottom: 2px; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index fd71876b0..a34bf64b0 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -39,7 +39,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( showTemplate = (): void => { const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null); - dragFactory && this.props.addDocTab(dragFactory, "onRight"); + dragFactory && this.props.addDocTab(dragFactory, "add:right"); } dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); @@ -63,15 +63,18 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( const label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); const color = StrCast(this.layoutDoc.color, this._foregroundColor); const backgroundColor = StrCast(this.layoutDoc._backgroundColor, StrCast(this.rootDoc.backgroundColor, this.props.backgroundColor?.(this.rootDoc, this.props.renderDepth))); - const shape = StrCast(this.layoutDoc.iconShape, "round"); - + const shape = StrCast(this.layoutDoc.iconShape, label ? "round" : "circle"); + const icon = StrCast(this.dataDoc.icon, "user") as any; + const presSize = shape === 'round' ? 25 : 30; + const presTrailsIcon = <img src={`/assets/${"presTrails.png"}`} + style={{ width: presSize, height: presSize, filter: `invert(${color === "white" ? "100%" : "0%"})`, marginBottom: "5px" }} />; const button = <button className={`menuButton-${shape}`} ref={this._ref} onContextMenu={this.specificContextMenu} style={{ boxShadow: this.layoutDoc.ischecked ? `4px 4px 12px black` : undefined, backgroundColor: this.layoutDoc.iconShape === "square" ? backgroundColor : "", }}> <div className="menuButton-wrap"> - {<FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={StrCast(this.dataDoc.icon, "user") as any} color={color} + {icon === 'pres-trail' ? presTrailsIcon : <FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={icon} color={color} size={this.layoutDoc.iconShape === "square" ? "sm" : "lg"} />} {!label ? (null) : <div className="fontIconBox-label" style={{ color, backgroundColor }}> {label} </div>} {this.props.Document.watchedDocuments ? <FontIconBadge collection={Cast(this.props.Document.watchedDocuments, Doc, null)} /> : (null)} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 216c5f39e..688bac725 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,8 +1,5 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEye } from '@fortawesome/free-regular-svg-icons'; -import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; import { observer } from "mobx-react"; import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; @@ -14,7 +11,8 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, Utils, returnZero } from '../../../Utils'; +import { emptyFunction, returnOne, returnZero, Utils } from '../../../Utils'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs } from '../../documents/Documents'; import { Networking } from '../../Network'; @@ -24,22 +22,16 @@ import { ContextMenu } from "../../views/ContextMenu"; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; -import FaceRectangles from './FaceRectangles'; +import { FaceRectangles } from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; -const requestImageSize = require('../../util/request-image-size'); const path = require('path'); const { Howl } = require('howler'); -library.add(faImage, faEye as any, faPaintBrush, faBrain); -library.add(faFileAudio, faAsterisk); - - export const pageSchema = createSchema({ - curPage: "number", + _curPage: "number", fitWidth: "boolean", googlePhotosUrl: "string", googlePhotosTags: "string" @@ -70,15 +62,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _dropDisposer?: DragManager.DragDropDisposer; + private _pathDisposer?: IReactionDisposer; @observable private _audioState = 0; @observable static _showControls: boolean; @observable uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { - this._dropDisposer && this._dropDisposer(); + this._dropDisposer?.(); ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document)); } + componentDidMount() { + this._pathDisposer = reaction(() => this.paths.length && this.resize(this.paths[0]), + () => true, + { fireImmediately: true }); + } + componentWillUnmount() { + this._pathDisposer?.(); + } + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { @@ -111,23 +113,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD }).then(function (stream) { gumStream = stream; recorder = new MediaRecorder(stream); - recorder.ondataavailable = async function (e: any) { - const formData = new FormData(); - formData.append("file", e.data); - const res = await fetch(Utils.prepend("/uploadFormData"), { - method: 'POST', - body: formData - }); - const files = await res.json(); - const url = Utils.prepend(files[0].path); - // upload to server with known URL - const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc)); - if (audioAnnos === undefined) { - this.dataDoc[this.fieldKey + "-audioAnnotations"] = new List([audioDoc]); - } else { - audioAnnos.push(audioDoc); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(e.data); + if (!(result instanceof Error)) { + const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = "layout"; + const audioAnnos = Cast(self.dataDoc[self.fieldKey + "-audioAnnotations"], listSpec(Doc)); + if (audioAnnos === undefined) { + self.dataDoc[self.fieldKey + "-audioAnnotations"] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } } }; runInAction(() => self._audioState = 2); @@ -158,7 +154,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (field) { const funcs: ContextMenuProps[] = []; funcs.push({ description: "Rotate Clockwise 90", event: this.rotate, icon: "expand-arrows-alt" }); - funcs.push({ description: "Make Background", event: () => { this.layoutDoc.isBackground = true; this.props.bringToFront?.(this.rootDoc); }, icon: "expand-arrows-alt" }); + funcs.push({ description: "Make Background", event: () => { this.layoutDoc._isBackground = true; this.props.bringToFront?.(this.rootDoc); }, icon: "expand-arrows-alt" }); if (!Doc.UserDoc().noviceMode) { funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); @@ -255,29 +251,29 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD resize = (imgPath: string) => { const basePath = imgPath.replace(/_[oms]./, ""); + const curPath = this.dataDoc[this.fieldKey + "-path"]; const cachedNativeSize = { - width: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, - height: basePath === this.dataDoc[this.fieldKey + "-path"] ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, + width: basePath === curPath || !curPath ? NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]) : 0, + height: basePath === curPath || !curPath ? NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]) : 0, }; const docAspect = this.layoutDoc[HeightSym]() / this.layoutDoc[WidthSym](); const cachedAspect = cachedNativeSize.height / cachedNativeSize.width; if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) { if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) { - requestImageSize(imgPath).then(action((inquiredSize: any) => { - const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; - const rotatedNativeSize = { width: inquiredSize.width, height: inquiredSize.height }; - if (inquiredSize.orientation === 6 || rotation === 90 || rotation === 270) { - rotatedNativeSize.width = inquiredSize.height; - rotatedNativeSize.height = inquiredSize.width; - } - const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width; - if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { - this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; - this.dataDoc[this.fieldKey + "-nativeWidth"] = this.layoutDoc._nativeWidth = this.layoutDoc._width; - this.dataDoc[this.fieldKey + "-nativeHeight"] = this.layoutDoc._nativeHeight = this.layoutDoc._height; - this.dataDoc[this.fieldKey + "-path"] = basePath; - } - })).catch(console.log); + const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180; + const orientation = NumCast(this.dataDoc[this.fieldKey + "-nativeOrientation"]); + if (orientation === 6 || rotation === 90 || rotation === 270) { + this.layoutDoc._nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + this.layoutDoc._nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); + } else { + this.layoutDoc._nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]); + this.layoutDoc._nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]); + } + const rotatedAspect = NumCast(this.layoutDoc._nativeHeight) / NumCast(this.layoutDoc._nativeWidth); + if (this.layoutDoc[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) { + this.layoutDoc._height = this.layoutDoc[WidthSym]() * rotatedAspect; + this.dataDoc[this.fieldKey + "-path"] = basePath; + } } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) { this.layoutDoc._width = this.layoutDoc[WidthSym]() || cachedNativeSize.width; this.layoutDoc._height = this.layoutDoc[WidthSym]() * cachedAspect; @@ -398,8 +394,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD const { nativeWidth, nativeHeight } = this.nativeSize; const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]); const aspect = (rotation % 180) ? nativeHeight / nativeWidth : 1; - const shift = (rotation % 180) ? (nativeHeight - nativeWidth) * (1 - 1 / aspect) : 0; - this.resize(srcpath); let transformOrigin = "center center"; let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`; if (rotation === 90 || rotation === -270) { @@ -437,7 +431,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD > <FontAwesomeIcon className="imageBox-audioFont" style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }} - icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" /> + icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" /> </div>} {this.considerDownloadIcon} {this.considerGooglePhotosLink()} @@ -465,7 +459,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD transform: this.props.PanelWidth() ? undefined : `scale(${this.props.ContentScaling()})`, width: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, height: this.props.PanelWidth() ? undefined : `${100 / this.props.ContentScaling()}%`, - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined, + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined, borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px` }} > <CollectionFreeFormView {...this.props} @@ -489,6 +483,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index b732f5f83..c5ff42a1a 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -230,7 +230,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { openItems.push({ description: "Default Perspective", event: () => { this.props.addDocTab(this.props.Document, "close"); - this.props.addDocTab(this.fieldDocToLayout, "onRight"); + this.props.addDocTab(this.fieldDocToLayout, "add:right"); }, icon: "image" }); !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" }); diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 4568a6b16..74d10d087 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,7 +1,7 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import { Doc, Field, Opt } from '../../../fields/Doc'; -import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter } from '../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; @@ -46,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { if (value instanceof Doc) { e.stopPropagation(); e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.clientX, e.clientY); } } @@ -56,7 +56,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { Document: this.props.doc, DataDoc: this.props.doc, LibraryPath: [], - docFilters:returnEmptyFilter, + docFilters: returnEmptyFilter, + searchFilterDocs: returnEmptyDoclist, ContainingCollectionView: undefined, ContainingCollectionDoc: undefined, fieldKey: this.props.keyName, diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index b605df262..109a02df4 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -8,6 +8,7 @@ } .labelBox-mainButton { + max-width: 100%; width: fit-content; height: max-content; border-radius: inherit; diff --git a/src/client/views/nodes/LinkAnchorBox.scss b/src/client/views/nodes/LinkAnchorBox.scss index 42ef2958e..62ee9513c 100644 --- a/src/client/views/nodes/LinkAnchorBox.scss +++ b/src/client/views/nodes/LinkAnchorBox.scss @@ -22,6 +22,11 @@ padding-left: 2px; padding-top: 1px; } + .linkAnchorBox-button { + pointer-events: all; + position: relative; + display: inline-block; + } } .linkAnchorBox-cont-small { diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 50b2af0d7..10b6aa02e 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -73,7 +73,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false); if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) { this._timeout = setTimeout(action(() => { - DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, e.altKey ? "inTab" : "onRight")), false); + DocumentManager.Instance.FollowLink(this.rootDoc, anchorContainerDoc, document => this.props.addDocTab(document, StrCast(this.layoutDoc.linkOpenLocation, "add:right")), false); this._editing = false; }), 300 - (Date.now() - this._lastTap)); } @@ -87,14 +87,14 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch } openLinkDocOnRight = (e: React.MouseEvent) => { - this.props.addDocTab(this.rootDoc, "onRight"); + this.props.addDocTab(this.rootDoc, "add:right"); } openLinkTargetOnRight = (e: React.MouseEvent) => { const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null)); alias.isLinkButton = undefined; - alias.isBackground = undefined; + alias._isBackground = undefined; alias.layoutKey = "layout"; - this.props.addDocTab(alias, "onRight"); + this.props.addDocTab(alias, "add:right"); } @action openLinkEditor = action((e: React.MouseEvent) => { @@ -148,7 +148,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch }} > {!this._editing && !this._forceOpen ? (null) : <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => this._isOpen = true} onClose={action(() => this._isOpen = this._forceOpen = this._editing = false)}> - <span className="parentDocumentSelector-button" > + <span className="linkAnchorBox-button" > <FontAwesomeIcon icon={"eye"} size={"lg"} /> </span> </Flyout>} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 532e7dc15..a067f23af 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -26,6 +26,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( NativeWidth={returnZero} ignoreFields={Cast(this.props.Document.linkBoxExcludedKeys, listSpec("string"), null)} annotationsKey={""} + dontRegisterView={true} + renderDepth={this.props.renderDepth + 1} CollectionView={undefined} addDocument={returnFalse} removeDocument={returnFalse} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c4481b213..dddefc17f 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import wiki from "wikijs"; import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Cast, FieldValue, NumCast } from "../../../fields/Types"; -import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../../Utils"; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, returnEmptyDoclist } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { Transform } from "../../util/Transform"; @@ -22,6 +22,7 @@ interface Props { } @observer export class LinkDocPreview extends React.Component<Props> { + static TargetDoc: Doc | undefined; @observable public static LinkInfo: Opt<{ linkDoc?: Doc; addDocTab: (document: Doc, where: string) => boolean, linkSrc: Doc; href?: string; Location: number[] }>; @observable _targetDoc: Opt<Doc>; @observable _toolTipText = ""; @@ -40,14 +41,16 @@ export class LinkDocPreview extends React.Component<Props> { async followDefault() { DocumentLinksButton.EditLink = undefined; LinkDocPreview.LinkInfo = undefined; - this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "onRight"), false) : null; + this._targetDoc ? DocumentManager.Instance.FollowLink(this.props.linkDoc, this._targetDoc, doc => this.props.addDocTab(doc, "add:right"), false) : null; } + componentWillUnmount() { LinkDocPreview.TargetDoc = undefined; } componentDidUpdate() { this.updatePreview(); } componentDidMount() { this.updatePreview(); } async updatePreview() { const linkDoc = this.props.linkDoc; const linkSrc = this.props.linkSrc; + LinkDocPreview.TargetDoc = undefined; if (this.props.href) { if (this.props.href.startsWith("https://en.wikipedia.org/wiki/")) { wiki().page(this.props.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); @@ -59,7 +62,7 @@ export class LinkDocPreview extends React.Component<Props> { const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; runInAction(() => { this._toolTipText = ""; - this._targetDoc = target; + LinkDocPreview.TargetDoc = this._targetDoc = target; if (anchor !== this._targetDoc && anchor && this._targetDoc) { this._targetDoc._scrollY = NumCast(anchor?.y); } @@ -69,13 +72,13 @@ export class LinkDocPreview extends React.Component<Props> { pointerDown = (e: React.PointerEvent) => { if (this.props.linkDoc && this.props.linkSrc) { DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.linkSrc, - (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation)); } else if (this.props.href) { - this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, UseCors: true }), "onRight"); + this.props.addDocTab(Docs.Create.WebDocument(this.props.href, { title: this.props.href, _width: 200, _height: 400, useCors: true }), "add:right"); } } - width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); - height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); + width = () => Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)) - 16; + height = () => Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)) - 16 @computed get targetDocView() { return !this._targetDoc ? <div style={{ @@ -101,11 +104,12 @@ export class LinkDocPreview extends React.Component<Props> { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} renderDepth={0} - PanelWidth={() => this.width() - 16} //Math.min(350, NumCast(target._width, 350))} - PanelHeight={() => this.height() - 16} //Math.min(250, NumCast(target._height, 250))} + PanelWidth={this.width} //Math.min(350, NumCast(target._width, 350))} + PanelHeight={this.height} //Math.min(250, NumCast(target._height, 250))} focus={emptyFunction} whenActiveChanged={returnFalse} bringToFront={returnFalse} @@ -119,7 +123,7 @@ export class LinkDocPreview extends React.Component<Props> { return <div className="linkDocPreview" style={{ position: "absolute", left: this.props.location[0], - top: this.props.location[1], width: this.width(), height: this.height(), + top: this.props.location[1], width: this.width() + 16, height: this.height() + 16, zIndex: 1000, border: "8px solid white", borderRadius: "7px", boxShadow: "3px 3px 1.5px grey", diff --git a/src/client/views/nodes/MenuIconBox.scss b/src/client/views/nodes/MenuIconBox.scss deleted file mode 100644 index 1b72f5a8f..000000000 --- a/src/client/views/nodes/MenuIconBox.scss +++ /dev/null @@ -1,49 +0,0 @@ -.menuButton { - //padding: 7px; - padding-left: 7px; - width: 100%; - width: 60px; - height: 70px; - - .menuButton-wrap { - width: 45px; - /* padding: 5px; */ - touch-action: none; - background: black; - transform-origin: top left; - /* margin-bottom: 5px; */ - margin-top: 5px; - margin-right: 25px; - border-radius: 8px; - - &:hover { - background: rgb(61, 61, 61); - cursor: pointer; - } - } - - .menuButton-label { - color: white; - margin-right: 4px; - border-radius: 8px; - width: 42px; - position: relative; - text-align: center; - font-size: 8px; - margin-top: 1px; - letter-spacing: normal; - padding: 3px; - background-color: inherit; - } - - .menuButton-icon { - width: auto; - height: 35px; - padding: 5px; - } - - svg { - width: 95% !important; - height: 95%; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/MenuIconBox.tsx b/src/client/views/nodes/MenuIconBox.tsx deleted file mode 100644 index 5ed8a9b78..000000000 --- a/src/client/views/nodes/MenuIconBox.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { createSchema, makeInterface } from '../../../fields/Schema'; -import { StrCast } from '../../../fields/Types'; -import { DocComponent } from '../DocComponent'; -import { FieldView, FieldViewProps } from './FieldView'; -import './MenuIconBox.scss'; -const MenuIconSchema = createSchema({ - icon: "string" -}); - -type MenuIconDocument = makeInterface<[typeof MenuIconSchema]>; -const MenuIconDocument = makeInterface(MenuIconSchema); -@observer -export class MenuIconBox extends DocComponent<FieldViewProps, MenuIconDocument>(MenuIconDocument) { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MenuIconBox, fieldKey); } - _ref: React.RefObject<HTMLButtonElement> = React.createRef(); - - render() { - - const color = this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) === "lightgrey" ? "black" : "white"; - const menuBTN = <div className="menuButton" style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) }}> - <div className="menuButton-wrap" - style={{ backgroundColor: this.props.backgroundColor?.(this.props.Document, this.props.renderDepth) }} > - <FontAwesomeIcon className="menuButton-icon" icon={StrCast(this.dataDoc.icon, "user") as any} color={color} size="lg" /> - <div className="menuButton-label" style={{ color: color }}> {this.dataDoc.title} </div> - </div> - </div>; - - return menuBTN; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 255a1b2d0..a2b406c3f 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -21,6 +21,7 @@ import { KeyCodes } from '../../util/KeyCodes'; import "./PDFBox.scss"; import React = require("react"); import { documentSchema } from '../../../fields/documentSchemas'; +import { CollectionViewType } from '../collections/CollectionView'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -99,20 +100,28 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); }; public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); }; public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); }; - public backPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }; - public forwardPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }; - public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); }; + public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; + public forwardPage = () => { this.Document._curPage = (this.Document._curPage || 1) + 1; return true; }; + public gotoPage = (p: number) => { this.Document._curPage = p; }; @undoBatch onKeyDown = action((e: KeyboardEvent) => { + let processed = false; if (e.key === "f" && e.ctrlKey) { this._searching = true; setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100); + processed = true; + } + if (e.key === "PageDown") processed = this.forwardPage(); + if (e.key === "PageUp") processed = this.backPage(); + if (e.target instanceof HTMLInputElement || this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { + if (e.key === "ArrowDown" || e.key === "ArrowRight") processed = this.forwardPage(); + if (e.key === "ArrowUp" || e.key === "ArrowLeft") processed = this.backPage(); + } + if (processed) { e.stopImmediatePropagation(); e.preventDefault(); } - if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") this.forwardPage(); - if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") this.backPage(); }); @undoBatch @@ -150,7 +159,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum </button> </>; const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`; - const curPage = this.Document.curPage || 1; + const curPage = this.Document._curPage || 1; return !this.active() ? (null) : (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true} onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none" }}> @@ -177,7 +186,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <div className="pdfBox-pageNums"> <input value={curPage} - onChange={e => this.gotoPage(Number(e.currentTarget.value))} + onChange={e => this.Document._curPage = Number(e.currentTarget.value)} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }} onClick={action(() => this._pageControls = !this._pageControls)} /> {this._pageControls ? pageBtns : (null)} @@ -246,7 +255,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded} setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView} renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} - addDocTab={this.props.addDocTab} focus={this.props.focus} docFilters={this.props.docFilters} + addDocTab={this.props.addDocTab} focus={this.props.focus} docFilters={this.props.docFilters} searchFilterDocs={this.props.searchFilterDocs} pinToPres={this.props.pinToPres} addDocument={this.addDocument} Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling} ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select} diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss index c4d8f1a4f..08160a2f4 100644 --- a/src/client/views/nodes/PresBox.scss +++ b/src/client/views/nodes/PresBox.scss @@ -488,7 +488,7 @@ $light-background: #ececec; max-width: 200px; overflow: visible; - + .presBox-dropdownOption { cursor: pointer; font-size: 11; @@ -967,10 +967,10 @@ $light-background: #ececec; /* border: solid 1px; */ color: white; /* box-shadow: black 0.4vw 0.4vw 0.8vw; */ - + .miniPresOverlay { display: grid; - grid-template-columns: auto auto auto auto auto auto auto auto; + grid-template-columns: auto auto auto auto auto auto auto auto auto auto; grid-template-rows: 100%; height: 100%; justify-items: center; @@ -1028,4 +1028,4 @@ $light-background: #ececec; background-color: #5a5a5a; } } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 5fc76223d..07b2d51d1 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -1,34 +1,33 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tooltip } from "@material-ui/core"; import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DocCastAsync, WidthSym } from "../../../fields/Doc"; -import { InkTool } from "../../../fields/InkField"; -import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../fields/Types"; -import { returnFalse, returnOne, numberRange, returnTrue } from "../../../Utils"; +import { ColorState, SketchPicker } from "react-color"; +import { Doc, DocCastAsync, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; -import { DocumentManager } from "../../util/DocumentManager"; -import { undoBatch } from "../../util/UndoManager"; -import { CollectionDockingView, DockedFrameRenderer } from "../collections/CollectionDockingView"; -import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { FieldView, FieldViewProps } from './FieldView'; -import { DocumentType } from "../../documents/DocumentTypes"; -import "./PresBox.scss"; -import { ViewBoxBaseComponent } from "../DocComponent"; -import { makeInterface, listSpec } from "../../../fields/Schema"; -import { Docs, DocUtils } from "../../documents/Documents"; +import { InkTool } from "../../../fields/InkField"; +import { List } from "../../../fields/List"; import { PrefetchProxy } from "../../../fields/Proxy"; +import { listSpec, makeInterface } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; +import { Cast, NumCast, StrCast } from "../../../fields/Types"; +import { returnFalse, returnOne } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; -import { List } from "../../../fields/List"; -import { Tooltip } from "@material-ui/core"; -import { actionAsync } from "mobx-utils"; import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { TabDocView } from "../collections/TabDocView"; +import { ViewBoxBaseComponent } from "../DocComponent"; import { AudioBox } from "./AudioBox"; -import { DocumentView } from "./DocumentView"; -import { SketchPicker, ColorState } from "react-color"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./PresBox.scss"; type PresBoxSchema = makeInterface<[typeof documentSchema]>; const PresBoxDocument = makeInterface(documentSchema); @@ -58,9 +57,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @observable private expandBoolean: boolean = false; @observable private openMovementDropdown: boolean = false; @observable private openEffectDropdown: boolean = false; - @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); } @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); } + @computed get activeItem() { return Cast(this.childDocs[this.itemIndex], Doc, null); } + @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); } @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); } constructor(props: any) { super(props); @@ -86,14 +86,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } else { return undefined; } } @computed get isPres(): boolean { + document.removeEventListener("keydown", this.keyEvents, true); if (this.selectedDoc?.type === DocumentType.PRES) { - document.removeEventListener("keydown", this.keyEvents, true); document.addEventListener("keydown", this.keyEvents, true); return true; - } else { - document.removeEventListener("keydown", this.keyEvents, true); - return false; } + return false; } @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } @@ -108,6 +106,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.rootDoc._replacedChrome = "replaced"; this.layoutDoc.presStatus = "edit"; this.layoutDoc._gridGap = 5; + if (!DocListCast((Doc.UserDoc().myPresentations as Doc).data).includes(this.rootDoc)) { + Doc.AddDocToList(Doc.UserDoc().myPresentations as Doc, "data", this.rootDoc); + } } updateCurrentPresentation = () => { @@ -123,37 +124,40 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> next = () => { this.updateCurrentPresentation(); const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const presTargetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - const childDocs = DocListCast(presTargetDoc[Doc.LayoutFieldKey(presTargetDoc)]); - const currentFrame = Cast(presTargetDoc.currentFrame, "number", null); - const lastFrame = Cast(presTargetDoc.lastFrame, "number", null); - const curFrame = NumCast(presTargetDoc.currentFrame); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); + const currentFrame = Cast(targetDoc._currentFrame, "number", null); + const lastFrame = Cast(targetDoc.lastFrame, "number", null); + const curFrame = NumCast(targetDoc._currentFrame); let internalFrames: boolean = false; - if (presTargetDoc.presProgressivize || activeItem.zoomProgressivize || presTargetDoc.scrollProgressivize) internalFrames = true; + if (targetDoc.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true; // Case 1: There are still other frames and should go through all frames before going to next slide if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) { - presTargetDoc._viewTransition = "all 1s"; - setTimeout(() => presTargetDoc._viewTransition = undefined, 1010); - presTargetDoc.currentFrame = curFrame + 1; - if (presTargetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(presTargetDoc, currentFrame); - if (presTargetDoc.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, presTargetDoc); - else presTargetDoc.editing = true; - if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(presTargetDoc); + targetDoc._viewTransition = "all 1s"; + setTimeout(() => targetDoc._viewTransition = undefined, 1010); + targetDoc._currentFrame = curFrame + 1; + if (targetDoc.scrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(targetDoc, currentFrame); + if (targetDoc.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc); + else targetDoc.editing = true; + if (activeItem.zoomProgressivize) this.zoomProgressivizeNext(targetDoc); // Case 2: Audio or video therefore wait to play the audio or video before moving on - } else if ((presTargetDoc.type === DocumentType.AUDIO) && !this._moveOnFromAudio && this.layoutDoc.presStatus !== 'auto') { + } else if ((targetDoc.type === DocumentType.AUDIO) && !this._moveOnFromAudio && this.layoutDoc.presStatus !== 'auto') { AudioBox.Instance.playFrom(0); this._moveOnFromAudio = true; // Case 3: No more frames in current doc and next slide is defined, therefore move to next slide } else if (this.childDocs[this.itemIndex + 1] !== undefined) { const nextSelected = this.itemIndex + 1; - if (presTargetDoc.type === DocumentType.AUDIO) AudioBox.Instance.pause(); + if (targetDoc.type === DocumentType.AUDIO) AudioBox.Instance.pause(); this.gotoDocument(nextSelected, this.itemIndex); const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null); if (activeNext && targetNext.type === DocumentType.AUDIO && activeNext.playAuto) { AudioBox.Instance.playFrom(0); this._moveOnFromAudio = true; } else this._moveOnFromAudio = false; + } else if (this.childDocs[this.itemIndex + 1] === undefined && this.layoutDoc.presLoop) { + const nextSelected = 0; + this.gotoDocument(0, this.itemIndex); } } @@ -166,19 +170,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action back = () => { this.updateCurrentPresentation(); - const docAtCurrent = this.childDocs[this.itemIndex]; - const targetDoc = Cast(docAtCurrent.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null); const lastFrame = Cast(targetDoc.lastFrame, "number", null); - const curFrame = NumCast(targetDoc.currentFrame); + const curFrame = NumCast(targetDoc._currentFrame); if (lastFrame !== undefined && curFrame >= 1) { - this.prevKeyframe(targetDoc, docAtCurrent); - } else if (docAtCurrent) { + this.prevKeyframe(targetDoc, activeItem); + } else if (activeItem) { let prevSelected = this.itemIndex; prevSelected = Math.max(0, prevSelected - 1); this.gotoDocument(prevSelected, this.itemIndex); - if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc.currentFrame = NumCast(prevTargetDoc.lastFrame); + if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame); } } @@ -209,8 +213,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * on the right. */ navigateToElement = async (curDoc: Doc) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const srcContext = await DocCastAsync(targetDoc.context); const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); const collectionDocView = presCollection ? await DocumentManager.Instance.getDocumentView(presCollection) : undefined; @@ -233,7 +237,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // If openDocument is selected then it should open the document for the user if (activeItem.openDocument) { - this.props.addDocTab(activeItem, "replace"); + this.props.addDocTab(activeItem, "replace:right"); } else //docToJump stayed same meaning, it was not in the group or was the last element in the group if (activeItem.zoomProgressivize && this.rootDoc.presStatus !== 'edit') { @@ -254,9 +258,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // adjust the pan and scale to that of the pinView when it was added. // TODO: Add option to remove presPinView if (activeItem.presPinView) { - targetDoc._panX = activeItem.presPinViewX; - targetDoc._panY = activeItem.presPinViewY; - targetDoc._viewScale = activeItem.presPinViewScale; + // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target + const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; + bestTarget && runInAction(() => { + bestTarget._viewTransition = "all 1s"; + bestTarget._panX = activeItem.presPinViewX; + bestTarget._panY = activeItem.presPinViewY; + bestTarget._viewScale = activeItem.presPinViewScale; + }); + //setTimeout(() => targetDoc._viewTransition = undefined, 1010); } // If website and has presWebsite data associated then on click it should // go back to that specific website @@ -271,7 +281,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * @param presTargetDoc: document for which internal zoom is used */ zoomProgressivizeNext = (activeItem: Doc) => { - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; const srcContext = Cast(targetDoc?.context, Doc, null); const docView = DocumentManager.Instance.getDocumentView(targetDoc); const vfLeft = this.checkList(targetDoc, activeItem["viewfinder-left-indexed"]); @@ -346,8 +356,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action startAutoPres = (startSlide: number) => { this.updateCurrentPresentation(); - let activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - let targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + let activeItem: Doc = this.activeItem; + let targetDoc: Doc = this.targetDoc; let duration = NumCast(targetDoc.presDuration) + NumCast(targetDoc.presTransition); const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms)); const load = async () => { // Wrap the loop into an async function for this to work @@ -363,17 +373,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } } await timer(duration); this.next(); // then the created Promise can be awaited - if (i === this.childDocs.length - 1) setTimeout(() => { clearTimeout(this._presTimer); if (this.layoutDoc.presStatus === 'auto') this.layoutDoc.presStatus = "manual"; }, duration); + if (i === this.childDocs.length - 1) { + setTimeout(() => { + clearTimeout(this._presTimer); + if (this.layoutDoc.presStatus === 'auto' && !this.layoutDoc.presLoop) this.layoutDoc.presStatus = "manual"; + else if (this.layoutDoc.presLoop) this.startAutoPres(0); + }, duration); + } } }; + this.layoutDoc.presStatus = "auto"; + this.startPresentation(startSlide); + this.gotoDocument(startSlide, this.itemIndex); + load(); + } + + @action + pauseAutoPres = () => { if (this.layoutDoc.presStatus === "auto") { if (this._presTimer) clearTimeout(this._presTimer); this.layoutDoc.presStatus = "manual"; - } else { - this.layoutDoc.presStatus = "auto"; - this.startPresentation(startSlide); - this.gotoDocument(startSlide, this.itemIndex); - load(); + this.layoutDoc.presLoop = false; } } @@ -401,12 +421,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> startPresentation = (startIndex: number) => { this.updateCurrentPresentation(); this.childDocs.map(doc => { - const presTargetDoc = doc.presentationTargetDoc as Doc; + const tagDoc = doc.presentationTargetDoc as Doc; if (doc.presHideTillShownButton && this.childDocs.indexOf(doc) > startIndex) { - presTargetDoc.opacity = 0; + tagDoc.opacity = 0; } if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) { - presTargetDoc.opacity = 0; + tagDoc.opacity = 0; } }); } @@ -418,11 +438,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action updateMinimize = () => { + const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc); if (this.layoutDoc.inOverlay) { this.layoutDoc.presStatus = 'edit'; - Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); - CollectionDockingView.AddRightSplit(this.rootDoc); + Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); + CollectionDockingView.AddSplit(this.rootDoc, "right"); this.layoutDoc.inOverlay = false; + } else if (this.layoutDoc.context && docView) { + this.layoutDoc.presStatus = 'manual'; + const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250); + this.rootDoc.y = pt[1] + 10; + this.rootDoc._height = 35; + this.rootDoc._width = 250; + docView.props.removeDocument?.(this.layoutDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } else { this.layoutDoc.presStatus = 'manual'; const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); @@ -431,7 +461,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> this.rootDoc._height = 35; this.rootDoc._width = 250; this.props.addDocTab?.(this.rootDoc, "close"); - Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc); + Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc); } } @@ -453,31 +483,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * When the movement dropdown is changes */ @undoBatch - movementChanged = action((movement: string) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + updateMovement = action((movement: any, activeItem: Doc, targetDoc: Doc) => { switch (movement) { case 'zoom': //Pan and zoom - activeItem.presZoomButton = !activeItem.presZoomButton; - if (activeItem.presZoomButton) activeItem.presMovement = 'Zoom'; - else activeItem.presMovement = 'None'; activeItem.presNavButton = false; + activeItem.presZoomButton = !activeItem.presZoomButton; + targetDoc.presTransition = activeItem.presTransition; + if (activeItem.presZoomButton) activeItem.presMovement = 'zoom'; + else activeItem.presMovement = 'none'; break; case 'pan': //Pan activeItem.presZoomButton = false; activeItem.presNavButton = !activeItem.presNavButton; - if (activeItem.presNavButton) activeItem.presMovement = 'Pan'; - else activeItem.presMovement = 'None'; + targetDoc.presTransition = activeItem.presTransition; + if (activeItem.presNavButton) activeItem.presMovement = 'pan'; + else activeItem.presMovement = 'none'; break; case 'jump': //Jump Cut + activeItem.presTransition = targetDoc.presTransition; targetDoc.presTransition = 0; activeItem.presZoomButton = true; activeItem.presSwitchButton = !activeItem.presSwitchButton; - if (activeItem.presSwitchButton) activeItem.presMovement = 'Jump cut'; - else activeItem.presMovement = 'None'; + if (activeItem.presSwitchButton) activeItem.presMovement = 'jump'; + else activeItem.presMovement = 'none'; break; case 'none': default: - activeItem.presMovement = 'None'; + activeItem.presMovement = 'none'; activeItem.presZoomButton = false; activeItem.presNavButton = false; activeItem.presSwitchButton = false; @@ -485,6 +516,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } }); + setMovementName = action((movement: any, activeItem: Doc): string => { + let output: string = 'none'; + switch (movement) { + case 'zoom': output = 'Zoom'; break; //Pan and zoom + case 'pan': output = 'Pan'; break; //Pan + case 'jump': output = 'Jump cut'; break; //Jump Cut + case 'none': output = 'None'; break; //None + default: output = 'Zoom'; activeItem.presMovement = 'zoom'; break; //default set as zoom + } + return output; + }); + whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive)); // For dragging documents into the presentation trail addDocumentFilter = (doc: Doc | Doc[]) => { @@ -501,10 +544,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> return true; } childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement; - removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); + removeDocument = (doc: Doc) => { Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc); }; getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; - active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground) && + active = (outsideReaction?: boolean) => ((Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc._isBackground) && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false) /** @@ -526,10 +569,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> */ @computed get listOfSelected() { const list = this._selectedArray.map((doc: Doc, index: any) => { - const activeItem = Cast(doc, Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc!, Doc, null); + const curDoc = Cast(doc, Doc, null); + const tagDoc = Cast(curDoc.presentationTargetDoc!, Doc, null); return ( - <div className="selectedList-items">{index + 1}. {targetDoc.title}</div> + <div className="selectedList-items">{index + 1}. {tagDoc.title}</div> ); }); return list; @@ -563,8 +606,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => { this._selectedArray = []; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - if (activeItem) { + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + if (this.activeItem) { for (let i = Math.min(this.itemIndex, this.childDocs.indexOf(doc)); i <= Math.max(this.itemIndex, this.childDocs.indexOf(doc)); i++) { this._selectedArray.push(this.childDocs[i]); this._eleArray.push(ref); @@ -575,8 +618,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } // Key for when the presentaiton is active - @action - keyEvents = (e: KeyboardEvent) => { + @undoBatch + keyEvents = action((e: KeyboardEvent) => { + if (e.target instanceof HTMLInputElement) return; let handled = false; const anchorNode = document.activeElement as HTMLDivElement; if (anchorNode && anchorNode.className?.includes("lm_title")) return; @@ -592,29 +636,44 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> handled = true; } } if (e.keyCode === 37 || e.keyCode === 38) { // left(37) / a(65) / up(38) to go back - this.back(); if (this._presTimer) clearTimeout(this._presTimer); + this.back(); + if (this._presTimer) clearTimeout(this._presTimer); handled = true; } if (e.keyCode === 39 || e.keyCode === 40) { // right (39) / d(68) / down(40) to go to next - this.next(); if (this._presTimer) clearTimeout(this._presTimer); + this.next(); + if (this._presTimer) clearTimeout(this._presTimer); handled = true; } if (e.keyCode === 32) { // spacebar to 'present' or autoplay if (this.layoutDoc.presStatus !== "edit") this.startAutoPres(0); else this.layoutDoc.presStatus = "manual"; if (this._presTimer) clearTimeout(this._presTimer); handled = true; - } - if (e.keyCode === 8) { // delete selected items + } if (e.keyCode === 8) { // delete selected items if (this.layoutDoc.presStatus === "edit") { - this._selectedArray.forEach((doc, i) => { - this.removeDocument(doc); - }); + this._selectedArray.forEach((doc, i) => this.removeDocument(doc)); + this._selectedArray = []; + this._eleArray = []; + this._dragArray = []; handled = true; } - } - if (handled) { + } if (handled) { e.stopPropagation(); e.preventDefault(); + } if ((e.keyCode === 37 || e.keyCode === 38) && e.shiftKey) { // left(37) / a(65) / up(38) to go back + if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { + const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); + if ((index - 1) > 0) this._selectedArray.push(this.childDocs[index - 1]); + } + handled = true; + } if ((e.keyCode === 39 || e.keyCode === 40) && e.shiftKey) { // left(37) / a(65) / up(38) to go back + if (this.layoutDoc.presStatus === "edit" && this._selectedArray.length > 0) { + const index = this.childDocs.indexOf(this._selectedArray[this._selectedArray.length]); + if ((index - 1) > 0) { + this._selectedArray.push(this.childDocs[index - 1]); + } + } + handled = true; } - } + }); /** * @@ -636,12 +695,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get order() { const order: JSX.Element[] = []; this.childDocs.forEach((doc, index) => { - const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(targetDoc?.context, Doc, null); + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc?.context, Doc, null); // Case A: Document is contained within the colleciton if (this.rootDoc.presCollection === srcContext) { order.push( - <div className="pathOrder" style={{ top: NumCast(targetDoc.y), left: NumCast(targetDoc.x) }}> + <div className="pathOrder" style={{ top: NumCast(tagDoc.y), left: NumCast(tagDoc.x) }}> <div className="pathOrder-frame">{index + 1}</div> </div>); // Case B: Document is not inside of the collection @@ -666,11 +725,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get paths() { let pathPoints = ""; this.childDocs.forEach((doc, index) => { - const targetDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(targetDoc?.context, Doc, null); - if (targetDoc && this.rootDoc.presCollection === srcContext) { - const n1x = NumCast(targetDoc.x) + (NumCast(targetDoc._width) / 2); - const n1y = NumCast(targetDoc.y) + (NumCast(targetDoc._height) / 2); + const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); + const srcContext = Cast(tagDoc?.context, Doc, null); + if (tagDoc && this.rootDoc.presCollection === srcContext) { + const n1x = NumCast(tagDoc.x) + (NumCast(tagDoc._width) / 2); + const n1y = NumCast(tagDoc.y) + (NumCast(tagDoc._height) / 2); if (index = 0) pathPoints = n1x + "," + n1y; else pathPoints = pathPoints + " " + n1x + "," + n1y; } else { @@ -701,8 +760,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; activeItem.presFadeButton = !activeItem.presFadeButton; if (!activeItem.presFadeButton) { if (targetDoc) { @@ -722,9 +781,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 10000) timeInMS = 10000; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - if (targetDoc) targetDoc.presTransition = timeInMS; + if (this.targetDoc) this.targetDoc.presTransition = timeInMS; } // Converts seconds to ms and updates presDuration @@ -733,15 +790,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (change) timeInMS += change; if (timeInMS < 100) timeInMS = 100; if (timeInMS > 20000) timeInMS = 20000; - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - if (targetDoc) targetDoc.presDuration = timeInMS; + if (this.targetDoc) this.targetDoc.presDuration = timeInMS; } @computed get transitionDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (activeItem && targetDoc) { const transitionSpeed = targetDoc.presTransition ? NumCast(targetDoc.presTransition) / 1000 : 0.5; let duration = targetDoc.presDuration ? NumCast(targetDoc.presDuration) / 1000 : 2; @@ -753,21 +808,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Movement <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? 'solid 2px #5B9FDD' : 'solid 1px black' }}> - {activeItem.presMovement} + {this.setMovementName(activeItem.presMovement, activeItem)} <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? '#5B9FDD' : 'black' }} icon={"angle-down"} /> <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'None' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('none')}>None</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Zoom' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('zoom')}>Pan and Zoom</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Pan' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('pan')}>Pan</div> - <div className={`presBox-dropdownOption ${activeItem.presMovement === 'Jump cut' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.movementChanged('jump')}>Jump cut</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'none' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement('none', activeItem, targetDoc)}>None</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'zoom' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement('zoom', activeItem, targetDoc)}>Pan and Zoom</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'pan' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement('pan', activeItem, targetDoc)}>Pan</div> + <div className={`presBox-dropdownOption ${activeItem.presMovement === 'jump' ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement('jump', activeItem, targetDoc)}>Jump cut</div> </div> </div> - <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "inline-flex" : "none" }}> + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === 'pan' || activeItem.presMovement === 'zoom' ? "inline-flex" : "none" }}> <div className="presBox-subheading">Transition Speed</div> <div className="ribbon-property"> <input className="presBox-input" type="number" value={transitionSpeed} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e) => this.setTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -779,8 +834,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> </div> - <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> - <div className={`slider-headers ${activeItem.presMovement === 'Pan' || activeItem.presMovement === 'Zoom' ? "" : "none"}`}> + <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed} className={`toolbar-slider ${activeItem.presMovement === 'pan' || activeItem.presMovement === 'zoom' ? "" : "none"}`} id="toolbar-slider" onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setTransitionTime(e.target.value); }} /> + <div className={`slider-headers ${activeItem.presMovement === 'pan' || activeItem.presMovement === 'zoom' ? "" : "none"}`}> <div className="slider-text">Fast</div> <div className="slider-text">Medium</div> <div className="slider-text">Slow</div> @@ -797,7 +852,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-property"> <input className="presBox-input" type="number" value={duration} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e) => this.setDurationTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> @@ -858,10 +913,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get effectDirection(): string { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); let effect = ''; - switch (targetDoc.presEffectDirection) { + switch (this.targetDoc.presEffectDirection) { case 'left': effect = "Enter from left"; break; case 'right': effect = "Enter from right"; break; case 'top': effect = "Enter from top"; break; @@ -874,8 +927,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action applyTo = (array: Doc[]) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; array.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null); @@ -885,14 +938,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tagDoc.presEffect = targetDoc.presEffect; tagDoc.presEffectDirection = targetDoc.presEffectDirection; curDoc.presMovement = activeItem.presMovement; + this.updateMovement(activeItem.presMovement, curDoc, tagDoc); curDoc.presHideTillShownButton = activeItem.presHideTillShownButton; curDoc.presHideAfterButton = activeItem.presHideAfterButton; } }); } @computed get optionsDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (activeItem && targetDoc) { return ( <div> @@ -909,6 +963,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-button" style={{ backgroundColor: activeItem.presPinView ? "#aedef8" : "" }} onClick={() => { activeItem.presPinView = !activeItem.presPinView; + targetDoc.presPinView = activeItem.presPinView; if (activeItem.presPinView) { const x = targetDoc._panX; const y = targetDoc._panY; @@ -926,7 +981,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewX)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewX = Number(val); })} /> </div> </div> @@ -936,7 +991,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewY)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewY = Number(val); })} /> </div> </div> @@ -946,7 +1001,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <input className="presBox-input" style={{ textAlign: 'left', width: 50 }} type="number" value={NumCast(activeItem.presPinViewScale)} - onFocus={() => { document.removeEventListener("keydown", this.keyEvents, true); }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} /> </div> </div> @@ -996,9 +1051,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> <div className="ribbon-box"> Slide Title: <br></br> <input className="ribbon-textInput" placeholder="..." type="text" name="fname" - onFocus={() => { - document.removeEventListener("keydown", this.keyEvents, true); - }} + onFocus={() => document.removeEventListener("keydown", this.keyEvents, true)} onChange={(e) => { e.stopPropagation(); e.preventDefault(); @@ -1052,21 +1105,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> if (layout) doc = this.createTemplate(layout); if (freeform && layout) doc = this.createTemplate(layout, title); if (!freeform && !layout) doc = Docs.Create.TextDocument("", { _nativeWidth: 400, _width: 225, title: title }); - const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); - const data = Cast(presCollection?.data, listSpec(Doc)); - const presData = Cast(this.rootDoc.data, listSpec(Doc)); - if (data && doc && presData) { - data.push(doc); - DockedFrameRenderer.PinDoc(doc, false); - this.gotoDocument(this.childDocs.length, this.itemIndex); - } else { - this.props.addDocTab(doc as Doc, "onRight"); + if (doc) { + const presCollection = Cast(this.layoutDoc.presCollection, Doc, null); + const data = Cast(presCollection?.data, listSpec(Doc)); + const presData = Cast(this.rootDoc.data, listSpec(Doc)); + if (data && presData) { + data.push(doc); + TabDocView.PinDoc(doc, false); + this.gotoDocument(this.childDocs.length, this.itemIndex); + } else { + this.props.addDocTab(doc, "add:right"); + } } } createTemplate = (layout: string, input?: string) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; let x = 0; let y = 0; if (activeItem && targetDoc) { @@ -1120,9 +1175,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> // Case in which the document has keyframes to navigate to next key frame @undoBatch @action - nextKeyframe = (tagDoc: Doc, activeItem: Doc): void => { + nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc.currentFrame, "number", null); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { tagDoc.currentFrame = 0; CollectionFreeFormDocumentView.setupScroll(tagDoc, 0); @@ -1130,37 +1185,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame); CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc); - tagDoc.currentFrame = Math.max(0, (currentFrame || 0) + 1); - tagDoc.lastFrame = Math.max(NumCast(tagDoc.currentFrame), NumCast(tagDoc.lastFrame)); - if (activeItem.zoomProgressivize) { + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1); + tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame)); + if (curDoc.zoomProgressivize) { const resize = document.getElementById('resizable'); if (resize) { - resize.style.width = this.checkList(tagDoc, activeItem["viewfinder-width-indexed"]) + 'px'; - resize.style.height = this.checkList(tagDoc, activeItem["viewfinder-height-indexed"]) + 'px'; - resize.style.top = this.checkList(tagDoc, activeItem["viewfinder-top-indexed"]) + 'px'; - resize.style.left = this.checkList(tagDoc, activeItem["viewfinder-left-indexed"]) + 'px'; + resize.style.width = this.checkList(tagDoc, curDoc["viewfinder-width-indexed"]) + 'px'; + resize.style.height = this.checkList(tagDoc, curDoc["viewfinder-height-indexed"]) + 'px'; + resize.style.top = this.checkList(tagDoc, curDoc["viewfinder-top-indexed"]) + 'px'; + resize.style.left = this.checkList(tagDoc, curDoc["viewfinder-left-indexed"]) + 'px'; } } } @undoBatch @action - prevKeyframe = (tagDoc: Doc, activeItem: Doc): void => { + prevKeyframe = (tagDoc: Doc, actItem: Doc): void => { const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]); - const currentFrame = Cast(tagDoc.currentFrame, "number", null); + const currentFrame = Cast(tagDoc._currentFrame, "number", null); if (currentFrame === undefined) { - tagDoc.currentFrame = 0; + tagDoc._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice()); - tagDoc.currentFrame = Math.max(0, (currentFrame || 0) - 1); - if (activeItem.zoomProgressivize) { + tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1); + if (actItem.zoomProgressivize) { const resize = document.getElementById('resizable'); if (resize) { - resize.style.width = this.checkList(tagDoc, activeItem["viewfinder-width-indexed"]) + 'px'; - resize.style.height = this.checkList(tagDoc, activeItem["viewfinder-height-indexed"]) + 'px'; - resize.style.top = this.checkList(tagDoc, activeItem["viewfinder-top-indexed"]) + 'px'; - resize.style.left = this.checkList(tagDoc, activeItem["viewfinder-left-indexed"]) + 'px'; + resize.style.width = this.checkList(tagDoc, actItem["viewfinder-width-indexed"]) + 'px'; + resize.style.height = this.checkList(tagDoc, actItem["viewfinder-height-indexed"]) + 'px'; + resize.style.top = this.checkList(tagDoc, actItem["viewfinder-top-indexed"]) + 'px'; + resize.style.left = this.checkList(tagDoc, actItem["viewfinder-left-indexed"]) + 'px'; } } } @@ -1169,8 +1224,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> * Returns the collection type as a string for headers */ @computed get stringType(): string { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; let type: string = ''; if (activeItem) { switch (targetDoc.type) { @@ -1193,11 +1248,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get progressivizeDropdown() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); - const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black"; - const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black"; + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (activeItem && targetDoc) { + const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black"; + const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black"; return ( <div> <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> @@ -1209,7 +1264,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}> <div className="presBox-subheading">Active text color</div> - <div className="ribbon-property" style={{ backgroundColor: activeFontColor }} onClick={action(() => { console.log("hi"); this.openActiveColorPicker = !this.openActiveColorPicker; })}> + <div className="ribbon-property" style={{ backgroundColor: activeFontColor }} onClick={action(() => { this.openActiveColorPicker = !this.openActiveColorPicker; })}> </div> </div> {this.activeColorPicker} @@ -1237,7 +1292,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.editing ? "white" : "black", backgroundColor: targetDoc.editing ? "#5B9FDD" : "#AEDDF8" }} onClick={action(() => targetDoc.editing = !targetDoc.editing)} > - {NumCast(targetDoc.currentFrame)} + {NumCast(targetDoc._currentFrame)} </div> <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={e => { e.stopPropagation(); this.nextKeyframe(targetDoc, activeItem); }}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> @@ -1256,8 +1311,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action switchActive = (color: ColorState) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const val = String(color.hex); targetDoc["pres-text-color"] = val; return true; @@ -1265,16 +1320,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action switchPresented = (color: ColorState) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const val = String(color.hex); targetDoc["pres-text-viewed-color"] = val; return true; } @computed get activeColorPicker() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; return !this.openActiveColorPicker ? (null) : <SketchPicker onChange={this.switchActive} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', @@ -1283,8 +1338,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get viewedColorPicker() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; return !this.openViewedColorPicker ? (null) : <SketchPicker onChange={this.switchPresented} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', @@ -1308,8 +1363,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Toggle whether the user edits or not @action editZoomProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (!targetDoc.editZoomProgressivize) { if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true; targetDoc.zoomProgressivize = true; targetDoc.editZoomProgressivize = true; @@ -1323,8 +1378,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Toggle whether the user edits or not @action editScrollProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; if (!targetDoc.editScrollProgressivize) { if (!targetDoc.scrollProgressivize) { targetDoc.scrollProgressivize = true; activeItem.scrollProgressivize = true; } targetDoc.editScrollProgressivize = true; @@ -1337,14 +1392,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeScroll = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const activeItem: Doc = this.activeItem; activeItem.scrollProgressivize = !activeItem.scrollProgressivize; - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize; - CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc.currentFrame), true); + CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame)); if (targetDoc.editScrollProgressivize) { targetDoc.editScrollProgressivize = false; - targetDoc.currentFrame = 0; + targetDoc._currentFrame = 0; targetDoc.lastFrame = 0; } } @@ -1353,14 +1408,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeZoom = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + const activeItem: Doc = this.activeItem; activeItem.zoomProgressivize = !activeItem.zoomProgressivize; - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize; - CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc, true); + CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc); if (activeItem.editZoomProgressivize) { activeItem.editZoomProgressivize = false; - targetDoc.currentFrame = 0; + targetDoc._currentFrame = 0; targetDoc.lastFrame = 0; } } @@ -1368,9 +1423,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> //Progressivize Child Docs @action editProgressivize = (e: React.MouseEvent) => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); - targetDoc.currentFrame = targetDoc.lastFrame; + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; + targetDoc._currentFrame = targetDoc.lastFrame; if (!targetDoc.editProgressivize) { if (!activeItem.presProgressivize) { activeItem.presProgressivize = true; targetDoc.presProgressivize = true; } targetDoc.editProgressivize = true; @@ -1382,21 +1437,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action progressivizeChild = (e: React.MouseEvent) => { e.stopPropagation(); - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null); + const activeItem: Doc = this.activeItem; + const targetDoc: Doc = this.targetDoc; const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); if (!activeItem.presProgressivize) { targetDoc.editing = false; activeItem.presProgressivize = true; targetDoc.presProgressivize = true; - targetDoc.currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(docs, docs.length, true); + targetDoc._currentFrame = 0; + docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true)); targetDoc.lastFrame = docs.length - 1; } else { targetDoc.editProgressivize = false; activeItem.presProgressivize = false; targetDoc.presProgressivize = false; - targetDoc.currentFrame = 0; + targetDoc._currentFrame = 0; targetDoc.lastFrame = 0; targetDoc.editing = true; } @@ -1438,18 +1493,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @action checkList = (doc: Doc, list: any): number => { const x: List<number> = list; - if (x && x.length >= NumCast(doc.currentFrame) + 1) { - return x[NumCast(doc.currentFrame)]; + if (x && x.length >= NumCast(doc._currentFrame) + 1) { + return x[NumCast(doc._currentFrame)]; } else if (x) { - x.length = NumCast(doc.currentFrame) + 1; - x[NumCast(doc.currentFrame)] = x[NumCast(doc.currentFrame) - 1]; - return x[NumCast(doc.currentFrame)]; + x.length = NumCast(doc._currentFrame) + 1; + x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1]; + return x[NumCast(doc._currentFrame)]; } else return 100; } @computed get progressivizeChildDocs() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]); const tags: JSX.Element[] = []; docs.forEach((doc, index) => { @@ -1457,7 +1511,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>); } tags.push( - <div className="progressivizeButton" onPointerLeave={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc.currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> + <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? "#aedff8" : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}> <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div> <div className="progressivizeButton-frame">{doc.appearFrame}</div> <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div> @@ -1469,8 +1523,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action nextAppearFrame = (doc: Doc, i: number): void => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); const appearFrame = Cast(doc.appearFrame, "number", null); if (appearFrame === undefined) { doc.appearFrame = 0; @@ -1482,8 +1536,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @undoBatch @action prevAppearFrame = (doc: Doc, i: number): void => { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); + // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); const appearFrame = Cast(doc.appearFrame, "number", null); if (appearFrame === undefined) { doc.appearFrame = 0; @@ -1604,12 +1658,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } @computed get playButtonFrames() { - const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null); - const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null); + const targetDoc: Doc = this.targetDoc; return ( <> - {targetDoc ? <div className="miniPres-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> - <div>{targetDoc.currentFrame}</div> + {this.targetDoc ? <div className="miniPres-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}> + <div>{targetDoc._currentFrame}</div> <div className="miniPres-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div> <div>{targetDoc.lastFrame}</div> </div> : null} @@ -1620,8 +1673,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get playButtons() { // Case 1: There are still other frames and should go through all frames before going to next slide return (<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== "edit" ? "inline-flex" : "none" }}> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="presPanel-divider"></div> <div className="presPanel-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <div className="presPanel-button" onClick={() => this.startAutoPres(this.itemIndex)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> <div className="presPanel-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="presPanel-divider"></div> <div className="presPanel-button-text" style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}> @@ -1636,6 +1691,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div>); } + @action + startOrPause = () => { + if (this.layoutDoc.presStatus === "manual") this.startAutoPres(this.itemIndex); + else this.pauseAutoPres(); + } + render() { // calling this method for keyEvents this.isPres; @@ -1645,8 +1706,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> return this.layoutDoc.inOverlay ? <div className="miniPres" style={{ width: 250, height: 35, background: '#323232', top: 0, zIndex: 3000000 }}> {<div className="miniPresOverlay"> + <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="miniPres-button" style={{ color: this.layoutDoc.presLoop ? '#AEDDF8' : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip> + <div className="miniPres-divider"></div> <div className="miniPres-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></div> - <div className="miniPres-button" onClick={() => this.startAutoPres(this.itemIndex)}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === "auto" ? "Pause" : "Autoplay"}</div></>}><div className="miniPres-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip> <div className="miniPres-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></div> <div className="miniPres-divider"></div> <div className="miniPres-button-text"> diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx index bd5b3bff4..8876b4879 100644 --- a/src/client/views/nodes/RadialMenuItem.tsx +++ b/src/client/views/nodes/RadialMenuItem.tsx @@ -1,13 +1,9 @@ import React = require("react"); -import { observable, action } from "mobx"; -import { observer } from "mobx-react"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { observer } from "mobx-react"; import { UndoManager } from "../../util/UndoManager"; -library.add(faAngleRight); - export interface RadialMenuProps { description: string; event: (stuff?: any) => void; diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 1cd29d795..5d51c420b 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -1,12 +1,11 @@ import React = require("react"); -import { library } from "@fortawesome/fontawesome-svg-core"; -import { faVideo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, IReactionDisposer, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { Doc } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; +import { InkTool } from "../../../fields/InkField"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast } from "../../../fields/Types"; import { VideoField } from "../../../fields/URLField"; @@ -18,14 +17,11 @@ import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxBaseComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; -import { InkTool } from "../../../fields/InkField"; const path = require('path'); type ScreenshotDocument = makeInterface<[typeof documentSchema]>; const ScreenshotDocument = makeInterface(documentSchema); -library.add(faVideo); - @observer export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) { private _reactionDisposer?: IReactionDisposer; @@ -77,7 +73,7 @@ export class ScreenshotBox extends ViewBoxBaseComponent<FieldViewProps, Screensh const spt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); imageSummary.x = spt[0]; imageSummary.y = spt[1]; - Cast(Cast(Doc.UserDoc().myOverlayDocuments, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); + Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null)?.data, listSpec(Doc), []).push(imageSummary); } else { this.props.addDocument?.(imageSummary); } diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index 45cdfc5ad..d13680046 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit } from '@fortawesome/free-regular-svg-icons'; import { runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -16,9 +14,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { Handle, Tick, TooltipRail, Track } from './SliderBox-components'; import './SliderBox.scss'; - -library.add(faEdit as any); - const SliderSchema = createSchema({ _sliderMin: "number", _sliderMax: "number", diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index ee92e517c..d50a10bdd 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -23,7 +23,7 @@ import { SnappingManager } from "../../util/SnappingManager"; const path = require('path'); export const timeSchema = createSchema({ - currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first + _currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first }); type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>; const VideoDocument = makeInterface(documentSchema, timeSchema); @@ -82,7 +82,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action public FullScreen() { this._fullScreen = true; this.player && this.player.requestFullscreen(); - this._youtubePlayer && this.props.addDocTab(this.rootDoc, "inTab"); + this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add"); } choosePath(url: string) { @@ -109,14 +109,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD if (!this._videoRef) { const b = Docs.Create.LabelDocument({ x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 1), - _width: 150, _height: 50, title: (this.layoutDoc.currentTimecode || 0).toString(), + _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(), }); b.isLinkButton = true; this.props.addDocument?.(b); DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot"); Networking.PostToServer("/youtubeScreenshot", { id: this.youtubeVideoId, - timecode: this.layoutDoc.currentTimecode + timecode: this.layoutDoc._currentTimecode }).then(response => { const resolved = response?.accessPaths?.agnostic?.client; if (resolved) { @@ -128,7 +128,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD //convert to desired file format const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, - const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc.currentTimecode || 0).toString().replace(/\./, "_"))); + const filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.rootDoc.title).replace(/\..*$/, "") + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"))); VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => { if (returnedFilename) { this.createRealSummaryLink(returnedFilename); @@ -144,7 +144,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const imageSummary = Docs.Create.ImageDocument(url, { _nativeWidth: this.layoutDoc._nativeWidth, _nativeHeight: this.layoutDoc._nativeHeight, x: (this.layoutDoc.x || 0) + width, y: (this.layoutDoc.y || 0), - _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc.currentTimecode || 0) + " image-" + _width: 150, _height: height / width * 150, title: "--snapshot" + (this.layoutDoc._currentTimecode || 0) + " image-" }); Doc.GetProto(imageSummary)["data-nativeWidth"] = this.layoutDoc._nativeWidth; Doc.GetProto(imageSummary)["data-nativeHeight"] = this.layoutDoc._nativeHeight; @@ -155,8 +155,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD @action updateTimecode = () => { - this.player && (this.layoutDoc.currentTimecode = this.player.currentTime); - this._youtubePlayer && (this.layoutDoc.currentTimecode = this._youtubePlayer.getCurrentTime()); + this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); + this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime()); } componentDidMount() { @@ -185,9 +185,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._videoRef = vref; if (vref) { this._videoRef!.ontimeupdate = this.updateTimecode; + // @ts-ignore vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); - this._reactionDisposer && this._reactionDisposer(); - this._reactionDisposer = reaction(() => (this.layoutDoc.currentTimecode || 0), + this._reactionDisposer?.(); + this._reactionDisposer = reaction(() => (this.layoutDoc._currentTimecode || 0), time => !this._playing && (vref.currentTime = time), { fireImmediately: true }); } } @@ -272,21 +273,21 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD const onYoutubePlayerReady = (event: any) => { this._reactionDisposer?.(); this._youtubeReactionDisposer?.(); - this._reactionDisposer = reaction(() => this.layoutDoc.currentTimecode, () => !this._playing && this.Seek((this.layoutDoc.currentTimecode || 0))); + this._reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek((this.layoutDoc._currentTimecode || 0))); this._youtubeReactionDisposer = reaction( - () => Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => !this.props.Document.isAnnotating && Doc.GetSelectedTool() === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, { events: { - 'onReady': onYoutubePlayerReady, - 'onStateChange': onYoutubePlayerStateChange, + 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady, + 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange, } }); } private get uIButtons() { - const curTime = (this.layoutDoc.currentTimecode || 0); + const curTime = (this.layoutDoc._currentTimecode || 0); return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} > <span>{"" + Math.round(curTime)}</span> <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> @@ -328,7 +329,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onResetMove = (e: PointerEvent) => { this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY); - this.Seek(Math.max(0, (this.layoutDoc.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); + this.Seek(Math.max(0, (this.layoutDoc._currentTimecode || 0) + Math.sign(e.movementX) * 0.0333)); e.stopImmediatePropagation(); } @@ -336,14 +337,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD onResetUp = (e: PointerEvent) => { document.removeEventListener("pointermove", this.onResetMove, true); document.removeEventListener("pointerup", this.onResetUp, true); - this._isResetClick < 10 && (this.layoutDoc.currentTimecode = 0); + this._isResetClick < 10 && (this.layoutDoc._currentTimecode = 0); } @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); - const start = untracked(() => Math.round((this.layoutDoc.currentTimecode || 0))); + const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.layoutDoc._nativeWidth || 640)} height={(this.layoutDoc._nativeHeight || 390)} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />; @@ -353,7 +354,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD addDocumentWithTimestamp(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - const curTime = (this.layoutDoc.currentTimecode || -1); + const curTime = (this.layoutDoc._currentTimecode || -1); curTime !== -1 && (doc.displayTimecode = curTime); }); return this.addDocument(doc); @@ -384,6 +385,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> {this.contentFunc} </CollectionFreeFormView> diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 875142169..134860d0a 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -70,7 +70,6 @@ } } - .webBox-overlay { width: 100%; height: 100%; @@ -82,61 +81,24 @@ background:lightGray; width: 100%; } - .webBox-freeze { - display: flex; - align-items: center; - justify-content: center; - margin-right: 5px; - width: 30px; - } - - .webBox-urlEditor { - position: relative; - opacity: 0.9; + .webBox-annotationToggle { z-index: 9001; - transition: top .5s; - - .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; - } - - .switchToText { - color: $main-accent; - } - - .switchToText:hover { - color: $dark-color; - } - } - - button:hover { - transform: scale(1); - } + position: absolute; + top: 2; + left: 2; + box-shadow: black 0.3em 0.3em 1em; + border-radius: 5px; + display: flex; + opacity: 0.3; + width: 25px; + height: 25px; + align-items: center; + > svg { + margin: auto; } } - - .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%; + .webBox-annotationToggle:hover { + opacity: 1; } .touch-iframe-overlay { diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 1393e7868..c5d7c3c9f 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -24,13 +24,14 @@ import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import Annotation from "../pdf/Annotation"; -import PDFMenu from "../pdf/PDFMenu"; +import { Annotation } from "../pdf/Annotation"; +import { PDFMenu } from "../pdf/PDFMenu"; import { PdfViewerMarquee } from "../pdf/PDFViewer"; import { FieldView, FieldViewProps } from './FieldView'; import "./WebBox.scss"; import "../pdf/PDFViewer.scss"; import React = require("react"); +import { Tooltip } from '@material-ui/core'; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -235,81 +236,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum e.stopPropagation(); } - toggleAnnotationMode = () => { - this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating; - } - - urlEditor() { - return ( - <div className="webBox-urlEditor" - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} style={{ top: this._collapsed ? -70 : 0 }}> - <div className="urlEditor"> - <div className="editorBase"> - <button className="editor-collapse" - style={{ - top: this._collapsed ? 70 : 0, - 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 className="webBox-buttons" - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} style={{ display: this._collapsed ? "none" : "flex" }}> - <div className="webBox-freeze" title={"Annotate"} style={{ background: this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > - <FontAwesomeIcon icon={faPen} size={"2x"} /> - </div> - <div className="webBox-freeze" title={"Select"} style={{ background: !this.layoutDoc.isAnnotating ? "lightBlue" : "gray" }} onClick={this.toggleAnnotationMode} > - <FontAwesomeIcon icon={faMousePointer} size={"2x"} /> - </div> - <input className="webpage-urlInput" - placeholder="ENTER URL" - value={this._url} - onDrop={this.onUrlDrop} - onDragOver={this.onUrlDragover} - onChange={this.onURLChange} - onKeyDown={this.onValueKeyDown} - onClick={(e) => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - maxWidth: "120px", - }}> - <button className="submitUrl" onClick={this.submitURL} - onDragOver={this.onUrlDragover} onDrop={this.onUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={this.back}> - <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> - </button> - <button className="submitUrl" onClick={this.forward}> - <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> - </button> - </div> - </div> - </div> - </div> + editToggleBtn() { + return <Tooltip title={<div className="dash-tooltip" >{`${this.props.Document.isAnnotating ? "Exit" : "Enter"} annotation mode`}</div>}> + <div className="webBox-annotationToggle" + style={{ color: this.props.Document.isAnnotating ? "black" : "white", backgroundColor: this.props.Document.isAnnotating ? "white" : "black" }} + onClick={action(() => this.layoutDoc.isAnnotating = !this.layoutDoc.isAnnotating)}> + <FontAwesomeIcon icon="edit" size="sm" /> </div> - ); + </Tooltip>; } - - @action - toggleCollapse = () => { - this._collapsed = !this._collapsed; - } - - - _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; @@ -438,7 +374,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.UseCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.UseCors = !this.layoutDoc.UseCors, icon: "snowflake" }); + funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); } @@ -453,7 +389,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (field instanceof HtmlField) { view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href; + const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; @@ -474,16 +410,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> {view} - </div>; + </div> {!frozen ? (null) : - <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc.isBackground ? undefined : "all" }} + <div className="webBox-overlay" style={{ pointerEvents: this.layoutDoc._isBackground ? undefined : "all" }} onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}> <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} > <div className="indicator" ref={this._iframeIndicatorRef}></div> <div className="dragger" ref={this._iframeDragRef}></div> </div> </div>} - {this.urlEditor()} </>); } @@ -560,12 +495,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]); clipDoc.rootDocument = targetDoc; targetDoc.layoutKey = "layout"; - const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection + const annotationDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color if (annotationDoc) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { - DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); + if (!e.aborted && e.annoDragData && !e.annoDragData.linkDocument) { + e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); annotationDoc.isLinkButton = true; } } @@ -682,7 +617,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum transform: `scale(${this.props.ContentScaling()})`, width: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", height: Number.isFinite(this.props.ContentScaling()) ? `${100 / this.props.ContentScaling()}% ` : "100%", - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }} onContextMenu={this.specificContextMenu}> <base target="_blank" /> @@ -690,7 +625,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <div className={"webBox-outerContent"} ref={this._outerRef} style={{ width: Number.isFinite(this.props.ContentScaling()) ? `${Math.max(100, 100 / this.props.ContentScaling())}% ` : "100%", - pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc.isBackground ? "all" : "none" + pointerEvents: this.layoutDoc.isAnnotating && !this.layoutDoc._isBackground ? "all" : "none" }} onWheel={e => e.stopPropagation()} onPointerDown={this.onMarqueeDown} @@ -709,7 +644,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum }}> <div className={"webBox-innerContent"} style={{ height: NumCast(this.layoutDoc.scrollHeight), - pointerEvents: this.layoutDoc.isBackground ? "none" : undefined + pointerEvents: this.layoutDoc._isBackground ? "none" : undefined }}> <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} @@ -733,6 +668,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum ScreenToLocalTransform={this.scrollXf} renderDepth={this.props.renderDepth + 1} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> </CollectionFreeFormView> </div> @@ -740,6 +676,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum {this.annotationLayer} <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} /> </div > + + {this.props.isSelected() ? this.editToggleBtn() : null} </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 212da3f3d..3b77735a7 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -5,13 +5,14 @@ import { Id } from "../../../../fields/FieldSymbols"; import { ObjectField } from "../../../../fields/ObjectField"; import { ComputedField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter } from "../../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero, returnEmptyFilter, returnEmptyDoclist } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; import { Transform } from "../../../util/Transform"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; interface IDashDocView { node: any; @@ -114,7 +115,7 @@ export class DashDocView extends React.Component<IDashDocView> { } /*endregion*/ - componentWillMount = () => { + componentWillUnmount = () => { this._reactionDisposer?.(); } @@ -175,7 +176,7 @@ export class DashDocView extends React.Component<IDashDocView> { const outerStyle = { position: "relative" as "relative", textIndent: "0", - border: "1px solid " + StrCast(this._textBox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")), + border: "1px solid " + StrCast(this._textBox.Document.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")), width: this.props.node.props.width, height: this.props.node.props.height, display: this.props.node.props.hidden ? "none" : "inline-block", @@ -202,7 +203,7 @@ export class DashDocView extends React.Component<IDashDocView> { ({ dim, color }) => { spanStyle.width = outerStyle.width = Math.max(20, dim[0]) + "px"; spanStyle.height = outerStyle.height = Math.max(20, dim[1]) + "px"; - outerStyle.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + outerStyle.border = "1px solid " + StrCast(finalLayout.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") { @@ -254,7 +255,8 @@ export class DashDocView extends React.Component<IDashDocView> { whenActiveChanged={returnFalse} bringToFront={emptyFunction} dontRegisterView={false} - docFilters={this.props.tbox?.props.docFilters||returnEmptyFilter} + docFilters={this.props.tbox?.props.docFilters || returnEmptyFilter} + searchFilterDocs={this.props.tbox?.props.searchFilterDocs || returnEmptyDoclist} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index 8ae71c035..f2658e77e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -169,7 +169,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna onPointerDownEnumerables = async (e: any) => { e.stopPropagation(); const collview = await DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]); - collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight"); + collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "add:right"); } @@ -183,7 +183,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna } if (container) { const alias = Doc.MakeAlias(container.props.Document); - alias.viewType = CollectionViewType.Time; + alias._viewType = CollectionViewType.Time; let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField)); if (!list) { alias._columnHeaders = list = new List<SchemaHeaderField>(); @@ -191,7 +191,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb")); list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb")); alias._pivotField = this._fieldKey.startsWith("#") ? "#" : this._fieldKey; - this.props.tbox.props.addDocTab(alias, "onRight"); + this.props.tbox.props.addDocTab(alias, "add:right"); } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index afdd8fea2..160f4ba72 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -32,8 +32,8 @@ .formattedTextBox-dictation { height: 12px; width: 10px; - top: 0px; - left: 0px; + bottom: 5px; + right: 8px; position: absolute; } } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 2ded33346..311143ff7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,5 +1,3 @@ -import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; @@ -22,7 +20,7 @@ import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; -import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { makeInterface } from "../../../../fields/Schema"; import { Cast, DateCast, NumCast, StrCast, ScriptCast, BoolCast } from "../../../../fields/Types"; import { TraceMobx, OVERRIDE_ACL, GetEffectiveAcl } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils, setupMoveUpEvents } from '../../../../Utils'; @@ -33,8 +31,8 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from "../../../util/DragManager"; import { makeTemplate } from '../../../util/DropConverter'; -import buildKeymap, { updateBullets } from "./ProsemirrorExampleTransfer"; -import RichTextMenu from './RichTextMenu'; +import { buildKeymap, updateBullets } from "./ProsemirrorExampleTransfer"; +import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from "./RichTextRules"; //import { DashDocView } from "./DashDocView"; @@ -61,9 +59,6 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin, findLinkMark } import React = require("react"); import { DocumentManager } from '../../../util/DocumentManager'; -library.add(faEdit); -library.add(faSmile, faTextHeight, faUpload); - export interface FormattedTextBoxProps { makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text hideOnLeave?: boolean; // used by DocumentView for setting caption's hide on leave (bcz: would prefer to have caption-hideOnLeave field set or something similar) @@ -99,8 +94,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _linkTime: number | null = null; private _pause: boolean = false; - @computed get _recording() { return this.dataDoc.audioState === "recording"; } - set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } + @computed get _recording() { return this.dataDoc?.audioState === "recording"; } + set _recording(value) { + this.dataDoc.audioState = value ? "recording" : undefined; + } @observable private _entered = false; @@ -225,7 +222,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); const allLinks = [{ href: Utils.prepend("/doc/" + id), title: value, targetId: id }]; - const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "onRight", title: value }); + const link = this._editorView.state.schema.marks.linkAnchor.create({ allLinks, location: "add:right", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); @@ -350,7 +347,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp updateTitle = () => { if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing - StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) { + StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.dataDoc["title-custom"] && + Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey) { let node = this._editorView.state.doc; while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild; const str = node.textContent; @@ -374,10 +372,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link)); } } - public highlightSearchTerms = (terms: string[], alt: boolean) => { + public highlightSearchTerms = (terms: string[], backward: boolean) => { if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); @@ -385,31 +381,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let tr = this._editorView.state.tr; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); - if (BoolCast(Doc.GetProto(this.dataDoc).resetSearch) === true) { - this._searchIndex = 0; - Doc.GetProto(this.dataDoc).resetSearch = undefined; - } - else { - this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - if (alt === true) { - if (this._searchIndex > 1) { - this._searchIndex += -2; - } - else if (this._searchIndex === 1) { - this._searchIndex = length - 1; - } - else if (this._searchIndex === 0 && length !== 1) { - this._searchIndex = length - 2; - } - + this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; + if (backward === true) { + if (this._searchIndex > 1) { + this._searchIndex += -2; } + else if (this._searchIndex === 1) { + this._searchIndex = length - 1; + } + else if (this._searchIndex === 0 && length !== 1) { + this._searchIndex = length - 2; + } + } const lastSel = Math.min(flattened.length - 1, this._searchIndex); flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark)); flattened[lastSel] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); - - console.log(this._searchIndex); } } @@ -472,12 +460,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else if (de.complete.linkDragData) { de.complete.linkDragData.linkDropCallback = this.linkDrop; } + else if (de.complete.annoDragData) { + de.complete.annoDragData.linkDropCallback = this.linkDrop; + } } - linkDrop = (data: DragManager.LinkDragData) => { + linkDrop = (data: { linkDocument?: Doc }) => { const linkDoc = data.linkDocument!; const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-"; const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : ""; - this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id); + this.makeLinkToSelection(linkDoc[Id], anchor1Title, "add:right", anchor1Id); } getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null { @@ -511,10 +502,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (node.isTextblock) { let index = 0, foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); - while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { - const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); - ret.push(sel); - index = index + foundAt + find.length; + const regexp = new RegExp(find.replace("*", ""), "i"); + if (regexp) { + while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) { + const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); + ret.push(sel); + index = index + foundAt + find.length; + } } } else { node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find))); @@ -664,7 +658,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const newBullets: Doc[] = this.recursiveProgressivize(1, list)[0]; mainBulletList.push.apply(mainBulletList, newBullets); } - console.log(mainBulletList.length); const title = Docs.Create.TextDocument(StrCast(this.rootDoc.title), { title: "Title", _width: 800, _height: 70, x: 20, y: -10, _fontSize: '20pt', backgroundColor: "rgba(0,0,0,0)", appearFrame: 0, _fontWeight: 700 }); mainBulletList.push(title); const doc = Docs.Create.FreeformDocument(mainBulletList, { @@ -719,7 +712,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp recordDictation = () => { DictationManager.Controls.listen({ - interimHandler: this.setCurrentBulletContent, + interimHandler: this.setDictationContent, continuous: { indefinite: false }, }).then(results => { if (results && [DictationManager.Controls.Infringed].includes(results)) { @@ -730,22 +723,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); }; - recordBullet = async () => { - const completedCue = "end session"; - const results = await DictationManager.Controls.listen({ - interimHandler: this.setCurrentBulletContent, - continuous: { indefinite: false }, - terminators: [completedCue, "bullet", "next"] - }); - if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) { - DictationManager.Controls.stop(); - return; - } - this.nextBullet(this._editorView!.state.selection.to); - setTimeout(this.recordBullet, 2000); - } - - setCurrentBulletContent = (value: string) => { + setDictationContent = (value: string) => { if (this._editorView) { const state = this._editorView.state; const now = Date.now(); @@ -760,33 +738,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } - const recordingStart = DateCast(this.props.Document.recordingStart).date.getTime(); - this._break = false; - value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; const from = state.selection.from; - const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); - this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1))); - } - } - - nextBullet = (pos: number) => { - if (this._editorView) { - const frag = Fragment.fromArray(this.newListItems(2)); - if (this._editorView.state.doc.resolve(pos).depth >= 2) { - const slice = new Slice(frag, 2, 2); - let state = this._editorView.state; - this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); - pos += 4; - state = this._editorView.state; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos))); + this._break = false; + if (this.props.Document.recordingStart) { + const recordingStart = DateCast(this.props.Document.recordingStart)?.date.getTime(); + value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value; } + const tr = state.tr.insertText(value).addMark(from, from + value.length + 1, mark); + this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, from + value.length + 1))); } } - private newListItems = (count: number) => { - return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create())); - } - _keymap: any = undefined; _rules: RichTextRules | undefined; @computed get config() { @@ -852,13 +814,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (linkDoc) { const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-"; const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : ""; - this.makeLinkToSelection(linkDoc[Id], anchor2Title, "onRight", anchor2Id); + this.makeLinkToSelection(linkDoc[Id], anchor2Title, "add:right", anchor2Id); } }, { fireImmediately: true } ); this._disposers.editorState = reaction( () => { + if (!this.dataDoc || !this.layoutDoc) return undefined; if (this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey + "-textTemplate"]) { return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data; } @@ -894,13 +857,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } ); this._disposers.autoHeight = reaction( - () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight], - () => setTimeout(() => this.tryUpdateHeight(), 0) + () => ({ + width: NumCast(this.layoutDoc._width), + autoHeight: this.layoutDoc?._autoHeight + }), + ({ width, autoHeight }) => width !== undefined && setTimeout(() => this.tryUpdateHeight(), 0) ); this._disposers.height = reaction( - () => this.layoutDoc[HeightSym](), + () => NumCast(this.layoutDoc._height), action(height => { - if (height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { + if (height !== undefined && height <= 20 && height < NumCast(this.layoutDoc._delayAutoHeight, 20)) { this.layoutDoc._delayAutoHeight = height; } }) @@ -908,36 +874,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.setupEditor(this.config, this.props.fieldKey); - this._disposers.searchAlt = reaction(() => this.rootDoc.searchMatchAlt, - search => search ? this.highlightSearchTerms([Doc.SearchQuery()], false) : this.unhighlightSearchTerms(), - { fireImmediately: true }); - this._disposers.search = reaction(() => this.rootDoc.searchMatch, - search => search ? this.highlightSearchTerms([Doc.SearchQuery()], true) : this.unhighlightSearchTerms(), - { fireImmediately: this.rootDoc.searchMatch ? true : false }); - - this._disposers.record = reaction(() => this._recording, - () => { - if (this._recording) { - setTimeout(action(() => { - this.stopDictation(true); - setTimeout(() => this.recordDictation(), 500); - }), 500); - } else setTimeout(() => this.stopDictation(true), 0); - } - ); + this._disposers.search = reaction(() => Doc.IsSearchMatch(this.rootDoc), + search => search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms(), + { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false }); + + this._disposers.selected = reaction(() => this.props.isSelected(), action(() => this._recording = false)); + + if (!this.props.dontRegisterView) { + this._disposers.record = reaction(() => this._recording, + () => { + if (this._recording) { + setTimeout(action(() => { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 500); + }), 500); + } else setTimeout(() => this.stopDictation(true), 0); + } + ); + } this._disposers.scrollToRegion = reaction( () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { const findLinkFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; + let offset = 0; frag.forEach((node, index) => { const examinedNode = findLinkNode(node, editor); if (examinedNode?.textContent) { nodes.push(examinedNode); - start += index; + offset = index; } }); - return { frag: Fragment.fromArray(nodes), start: start }; + return { frag: Fragment.fromArray(nodes), start: start + offset }; }; const findLinkNode = (node: Node, editor: EditorView) => { if (!node.isText) { @@ -949,7 +917,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return linkIndex !== -1 && marks[linkIndex].attrs.allLinks.find((item: { href: string }) => scrollToLinkID === item.href.replace(/.*\/doc\//, "")) ? node : undefined; }; - let start = 0; + const start = 0; if (this._editorView && scrollToLinkID) { const editor = this._editorView; const ret = findLinkFrag(editor.state.doc.content, editor); @@ -1036,7 +1004,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } }, 0); dataDoc.title = exportState.title; - this.rootDoc.customTitle = true; + this.dataDoc["title-custom"] = true; dataDoc.unchanged = true; } else { delete dataDoc[GoogleRef]; @@ -1131,7 +1099,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }]; - const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "onRight", title, docref: true }); + const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); } @@ -1278,10 +1246,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp FormattedTextBoxComment.Hide(); if (FormattedTextBoxComment.linkDoc) { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + this.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "add" : "add:right"); } else { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, this.props.Document, - (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + (doc: Doc, followLinkLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation)); } } @@ -1307,7 +1275,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // jump rich text menu to this textbox const bounds = this._ref.current?.getBoundingClientRect(); - if (bounds && this.layoutDoc._chromeStatus !== "disabled") { + if (bounds && this.layoutDoc._chromeStatus !== "disabled" && RichTextMenu.Instance) { const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width); let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height); if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) { @@ -1411,11 +1379,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + menuPlugin: any; + richTextMenuPlugin() { + const self = this; return new Plugin({ view(newView) { - RichTextMenu.Instance?.changeView(newView); - return RichTextMenu.Instance; + self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + return self.menuPlugin = new RichTextMenuPlugin({ editorProps: this.props }); } }); } @@ -1432,13 +1403,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } return wasUndoing; } + public static LiveTextUndo: UndoManager.Batch | undefined; public static HadSelection: boolean = false; onBlur = (e: any) => { FormattedTextBox.HadSelection = window.getSelection()?.toString() !== ""; - //DictationManager.Controls.stop(false); this.endUndoTypingBatch(); this.doLinkOnDeselect(); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; // move the richtextmenu offscreen //if (!RichTextMenu.Instance.Pinned) RichTextMenu.Instance.delayHide(); } @@ -1524,8 +1497,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp TraceMobx(); const scale = this.props.hideOnLeave ? 1 : this.props.ContentScaling() * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc.isBackground; - setTimeout(() => this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props), this.props.isSelected() ? 10 : 0); // need to make sure that we update a text box that is selected after updating the one that was deselected + const interactive = Doc.GetSelectedTool() === InkTool.None && !this.layoutDoc._isBackground; + this.props.isSelected() && setTimeout(() => this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props), 0); // need to make sure that we update a text box that is selected after updating the one that was deselected if (!this.props.isSelected() && FormattedTextBoxComment.textBox === this) { setTimeout(() => FormattedTextBoxComment.Hide(), 0); } @@ -1546,13 +1519,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp width: "100%", height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined, background: Doc.UserDoc().renderStyle === "comic" ? "transparent" : this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""), - opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"), pointerEvents: interactive ? undefined : "none", fontSize: Cast(this.layoutDoc._fontSize, "string", null), fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), - transition: "opacity 1s" }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} @@ -1616,14 +1587,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} /> </div>} {!this.layoutDoc._showAudio ? (null) : - <div className="formattedTextBox-dictation" - onPointerDown={e => { - runInAction(() => this._recording = !this._recording); - setTimeout(() => this._editorView!.focus(), 500); - e.stopPropagation(); - }} > + <div className="formattedTextBox-dictation" onClick={action(e => this._recording = !this._recording)} > <FontAwesomeIcon className="formattedTextBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> + style={{ + color: this._recording ? "red" : "blue", + transitionDelay: "0.6s", + opacity: this._recording ? 1 : 0.25, + }} + icon={"microphone"} size="sm" /> </div>} </div> </div> diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx index 6f3984f39..b4f648273 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx @@ -4,7 +4,7 @@ import { EditorView } from "prosemirror-view"; import * as ReactDOM from 'react-dom'; import { Doc, DocCastAsync, Opt } from "../../../../fields/Doc"; import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types"; -import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter } from "../../../../Utils"; +import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne, returnEmptyFilter, returnEmptyDoclist } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { DocumentManager } from "../../../util/DocumentManager"; import { schema } from "./schema_rts"; @@ -103,7 +103,7 @@ export class FormattedTextBoxComment { this.deleteLink(); } else if (FormattedTextBoxComment._followRef && FormattedTextBoxComment._followRef.contains(e.target as any)) { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "add" : "add:right"); } else { const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(FormattedTextBoxComment.linkDoc.anchor2, Doc) : (Cast(FormattedTextBoxComment.linkDoc.anchor1, Doc)) @@ -111,29 +111,29 @@ export class FormattedTextBoxComment { const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor; if (FormattedTextBoxComment.linkDoc.follow) { - if (FormattedTextBoxComment.linkDoc.follow === "Default") { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); + if (FormattedTextBoxComment.linkDoc.follow === "default") { + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "add:right"), false); } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in right tab") { - if (target) { textBox.props.addDocTab(target, "onRight"); } + if (target) { textBox.props.addDocTab(target, "add:right"); } } else if (FormattedTextBoxComment.linkDoc.follow === "Always open in new tab") { - if (target) { textBox.props.addDocTab(target, "inTab"); } + if (target) { textBox.props.addDocTab(target, "add"); } } } else { - DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "onRight"), false); + DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, doc => textBox.props.addDocTab(doc, "add:right"), false); } } } else { if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) { - textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight"); + textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "add" : "add:right"); } else { DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document, - (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation)); + (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation)); } } } } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) { - textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, UseCors: true }), "onRight"); + textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400, useCors: true }), "add:right"); } keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation( FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark); @@ -232,6 +232,16 @@ export class FormattedTextBoxComment { const mark = child ? findLinkMark(child.marks) : undefined; const href = (!mark?.attrs.docref || naft === nbef) && mark?.attrs.allLinks.find((item: { href: string }) => item.href)?.href || forceUrl; if (forceUrl || (href && child && nbef && naft && mark?.attrs.showPreview)) { + try { + ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); + } catch (e) { } + FormattedTextBoxComment.tooltip.removeChild(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltipText = document.createElement("div"); + FormattedTextBoxComment.tooltipText.style.width = "100%"; + FormattedTextBoxComment.tooltipText.style.height = "100%"; + FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis"; + FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText); + FormattedTextBoxComment.tooltipText.textContent = "external => " + href; (FormattedTextBoxComment.tooltipText as any).href = href; if (href.startsWith("https://en.wikipedia.org/wiki/")) { @@ -241,12 +251,9 @@ export class FormattedTextBoxComment { FormattedTextBoxComment.tooltipText.style.overflow = "hidden"; } if (href.indexOf(Utils.prepend("/doc/")) === 0) { + const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; FormattedTextBoxComment.tooltipText.textContent = "target not found..."; (FormattedTextBoxComment.tooltipText as any).href = ""; - const docTarget = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - try { - ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText); - } catch (e) { } docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => { if (linkDoc instanceof Doc) { (FormattedTextBoxComment.tooltipText as any).href = href; @@ -302,6 +309,7 @@ export class FormattedTextBoxComment { pinToPres={returnFalse} dontRegisterView={true} docFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} ContainingCollectionDoc={undefined} ContainingCollectionView={undefined} renderDepth={0} @@ -323,8 +331,8 @@ export class FormattedTextBoxComment { ReactDOM.render(docPreview, FormattedTextBoxComment.tooltipText); - FormattedTextBoxComment.tooltip.style.width = NumCast(target._width) ? `${NumCast(target._width)}` : "100%"; - FormattedTextBoxComment.tooltip.style.height = NumCast(target._height) ? `${NumCast(target._height)}` : "100%"; + FormattedTextBoxComment.tooltip.style.width = "100%"; + FormattedTextBoxComment.tooltip.style.height = "100%"; } } }); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 8faf752b4..c6bacc1a8 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -33,7 +33,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?: return tx2; }; -export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { +export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap { const keys: { [key: string]: any } = {}; function bind(key: string, cmd: any) { @@ -104,7 +104,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any //Command to create a new Tab with a PDF of all the command shortcuts bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _fitWidth: true, _width: 300, _height: 300 }); - props.addDocTab(newDoc, "onRight"); + props.addDocTab(newDoc, "add:right"); }); //Commands to modify BlockType @@ -143,7 +143,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; - newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; + newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } @@ -168,7 +168,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, props: any const layoutKey = StrCast(originalDoc.layoutKey); const newDoc = Doc.MakeCopy(originalDoc, true); newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = undefined; - newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10; + newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10; if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) { newDoc[layoutKey] = originalDoc[layoutKey]; } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index b683fb25d..307238ea1 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,8 +1,8 @@ import React = require("react"); -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faCaretDown, faChevronLeft, faEyeDropper, faHighlighter, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faItalic, faLink, faPaintRoller, faPalette, faStrikethrough, faSubscript, faSuperscript, faUnderline } from "@fortawesome/free-solid-svg-icons"; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, IReactionDisposer, reaction } from "mobx"; +import { Tooltip } from "@material-ui/core"; +import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -11,31 +11,28 @@ import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; -import { Cast, StrCast, BoolCast, NumCast } from "../../../../fields/Types"; +import { Cast, StrCast } from "../../../../fields/Types"; +import { TraceMobx } from "../../../../fields/util"; import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; -import AntimodeMenu from "../../AntimodeMenu"; +import { undoBatch, UndoManager } from "../../../util/UndoManager"; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu"; import { FieldViewProps } from "../FieldView"; import { FormattedTextBox, FormattedTextBoxProps } from "./FormattedTextBox"; import { updateBullets } from "./ProsemirrorExampleTransfer"; import "./RichTextMenu.scss"; import { schema } from "./schema_rts"; -import { TraceMobx } from "../../../../fields/util"; -import { UndoManager, undoBatch } from "../../../util/UndoManager"; -import { Tooltip } from "@material-ui/core"; const { toggleMark } = require("prosemirror-commands"); -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faOutdent, faIndent, faHandPointLeft, faHandPointRight, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller); - @observer -export default class RichTextMenu extends AntimodeMenu { +export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable - private view?: EditorView; + public view?: EditorView; public editorProps: FieldViewProps & FormattedTextBoxProps | undefined; public _brushMap: Map<string, Set<Mark>> = new Map(); @@ -78,7 +75,7 @@ export default class RichTextMenu extends AntimodeMenu { RichTextMenu.Instance = this; this._canFade = false; //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); - this.Pinned = true; + runInAction(() => this.Pinned = true); this.fontSizeOptions = [ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize }, @@ -156,22 +153,8 @@ export default class RichTextMenu extends AntimodeMenu { public delayHide = () => this._delayHide = true; @action - changeView(view: EditorView) { - if ((view as any)?.TextView?.props.isSelected(true)) { - this.view = view; - } - } - - update(view: EditorView, lastState: EditorState | undefined) { - RichTextMenu.Instance.updateFromDash(view, lastState, this.editorProps); - } - - @action - public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { - RichTextMenu.Instance.finalUpdateFromDash(view, lastState, props); - } - public async finalUpdateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { - if (!view || !(view as any).TextView?.props.isSelected(true)) { + public updateMenu(view: EditorView, lastState: EditorState | undefined, props: any) { + if (!view || !(view as any).TextView?.props.isSelected(true) || !view.hasFocus()) { return; } this.view = view; @@ -199,8 +182,7 @@ export default class RichTextMenu extends AntimodeMenu { this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "..."; // update link in current selection - const targetTitle = await this.getTextLinkTargetTitle(); - this.setCurrentLink(targetTitle); + this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); } setMark = (mark: Mark, state: EditorState<any>, dispatch: any, dontToggle: boolean = false) => { @@ -268,7 +250,9 @@ export default class RichTextMenu extends AntimodeMenu { const pos = this.view.state.selection.$from; const ref_node = this.reference_node(pos); if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { - ref_node.marks.forEach(m => { + const marks = Array.from(ref_node.marks); + marks.push(...(this.view.state.storedMarks as any)); + marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt"); @@ -443,14 +427,20 @@ export default class RichTextMenu extends AntimodeMenu { if ((this.view?.state.selection.$from.pos || 0) < 2) { this.TextView.layoutDoc._fontSize = mark.attrs.fontSize; } - this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch, true); + const fmark = view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }); + this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); + view.focus(); + this.updateMenu(view, undefined, this.props); } changeFontFamily = (mark: Mark, view: EditorView) => { if ((this.view?.state.selection.$from.pos || 0) < 2) { this.TextView.layoutDoc._fontFamily = mark.attrs.family; } - this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch, true); + const fmark = view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }); + this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); + view.focus(); + this.updateMenu(view, undefined, this.props); } // TODO: remove doesn't work @@ -486,6 +476,8 @@ export default class RichTextMenu extends AntimodeMenu { this.view.dispatch(tx3); } } + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); } insertSummarizer(state: EditorState<any>, dispatch: any) { @@ -690,16 +682,22 @@ export default class RichTextMenu extends AntimodeMenu { e.preventDefault(); e.stopPropagation(); self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus(); + if (self.view) { + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.view.focus(); + self.updateMenu(self.view, undefined, self.props); + } } function changeColor(e: React.PointerEvent, color: string) { e.preventDefault(); e.stopPropagation(); self.setActiveColor(color); self.TextView.endUndoTypingBatch(); - UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); - self.TextView.EditorView!.focus(); + if (self.view) { + UndoManager.RunInBatch(() => self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch), "rt menu color"); + self.view.focus(); + self.updateMenu(self.view, undefined, self.props); + } } // onPointerDown={onColorClick} @@ -812,7 +810,7 @@ export default class RichTextMenu extends AntimodeMenu { <div className="dropdown link-menu"> <p>Linked to:</p> <input value={link} placeholder="Enter URL" onChange={onLinkChange} /> - <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button> + <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button> <div className="divider"></div> <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button> </div>; @@ -859,7 +857,7 @@ export default class RichTextMenu extends AntimodeMenu { // TODO: should check for valid URL @undoBatch makeLinkToURL = (target: string, lcoation: string) => { - ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRight", "", target); + ((this.view as any)?.TextView as FormattedTextBox).makeLinkToSelection("", target, "onRadd:rightight", "", target); } @undoBatch @@ -988,7 +986,7 @@ export default class RichTextMenu extends AntimodeMenu { {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => this.activeFontSize = val)), this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => this.activeFontFamily = val)), <div className="richTextMenu-divider" key="divider 4" />, - this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", action((val: string) => this.activeListType = val)), + this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer), this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote), this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule), @@ -1020,6 +1018,7 @@ interface ButtonDropdownProps { dropdownContent: JSX.Element; openDropdownOnButton?: boolean; link?: boolean; + pdf?: boolean; } @observer @@ -1059,15 +1058,33 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> { }, 0); } + render() { return ( <div className="button-dropdown-wrapper" ref={node => this.ref = node}> - <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> - {this.props.button} - <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> - </div> + {!this.props.pdf ? + <div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}> + {this.props.button} + <div style={{ marginTop: "-8.5" }}><FontAwesomeIcon icon="caret-down" size="sm" /></div> + </div> + : + <> + {this.props.button} + <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </button> + </>} {this.showDropdown ? this.props.dropdownContent : (null)} </div> ); } +} + + +interface RichTextMenuPluginProps { + editorProps: any; +} +export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> { + render() { return null; } + update(view: EditorView, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index dc1d8a2c8..5c0505909 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -3,13 +3,13 @@ import { NodeSelection, TextSelection } from "prosemirror-state"; import { DataSym, Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { ComputedField } from "../../../../fields/ScriptField"; -import { Cast, NumCast } from "../../../../fields/Types"; +import { Cast, NumCast, StrCast } from "../../../../fields/Types"; import { returnFalse, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; import { FormattedTextBox } from "./FormattedTextBox"; import { wrappingInputRule } from "./prosemirrorPatches"; -import RichTextMenu from "./RichTextMenu"; +import { RichTextMenu } from "./RichTextMenu"; import { schema } from "./schema_rts"; import { List } from "../../../../fields/List"; @@ -92,7 +92,7 @@ export class RichTextRules { const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: "9pt", title: "inline comment" }); textDocInline.title = inlineFieldKey; // give the annotation its own title - textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc + textDocInline["title-custom"] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`); @@ -279,7 +279,7 @@ export class RichTextRules { DocUtils.Publish(target, docid, returnFalse, returnFalse); DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to"); }); - const link = state.schema.marks.linkAnchor.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid }); + const link = state.schema.marks.linkAnchor.create({ href: Utils.prepend("/doc/" + docid), location: "add:right", title: docid, targetId: docid }); return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link); } return state.tr; @@ -321,7 +321,11 @@ export class RichTextRules { (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - this.Document[DataSym]["#" + tag] = "."; + this.Document[DataSym]["#" + tag] = "#" + tag; + const tags = StrCast(this.Document.tags, ":"); + if (!tags.includes(`#${tag}:`)) { + this.Document[DataSym].tags = `"${tags + "#" + tag + ':'}"`; + } const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag }); return state.tr.deleteRange(start, end).insert(start, fieldView); }), diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx index 33a080fe4..f0bacb735 100644 --- a/src/client/views/nodes/formattedText/RichTextSchema.tsx +++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx @@ -13,6 +13,7 @@ import { Transform } from "../../../util/Transform"; import { DocumentView } from "../DocumentView"; import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; export class DashDocView { @@ -43,7 +44,7 @@ export class DashDocView { this._outer = document.createElement("span"); this._outer.style.position = "relative"; this._outer.style.textIndent = "0"; - this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); this._outer.style.width = node.attrs.width; this._outer.style.height = node.attrs.height; this._outer.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -126,7 +127,7 @@ export class DashDocView { this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => { this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px"; this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px"; - this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")); + this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); }, { fireImmediately: true }); const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => { @@ -155,6 +156,7 @@ export class DashDocView { bringToFront={emptyFunction} dontRegisterView={false} docFilters={this._textBox.props.docFilters} + searchFilterDocs={this._textBox.props.searchFilterDocs} ContainingCollectionView={this._textBox.props.ContainingCollectionView} ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc} ContentScaling={this.contentScaling} diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 1616500f6..64f7d27e5 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -148,7 +148,7 @@ export const nodes: { [index: string]: NodeSpec } = { alt: { default: null }, title: { default: null }, float: { default: "left" }, - location: { default: "onRight" }, + location: { default: "add:right" }, docid: { default: "" } }, group: "inline", @@ -177,7 +177,7 @@ export const nodes: { [index: string]: NodeSpec } = { height: { default: 100 }, title: { default: null }, float: { default: "right" }, - location: { default: "onRight" }, + location: { default: "add:right" }, hidden: { default: false }, fieldKey: { default: "" }, docid: { default: "" }, diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index d29b638e6..222a6cb0f 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -4,9 +4,9 @@ import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; -import { Cast, FieldValue, NumCast, StrCast } from "../../../fields/Types"; +import { Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../fields/Types"; import { DocumentManager } from "../../util/DocumentManager"; -import PDFMenu from "./PDFMenu"; +import { PDFMenu } from "./PDFMenu"; import "./Annotation.scss"; interface IAnnotationProps { @@ -19,7 +19,7 @@ interface IAnnotationProps { } @observer -export default +export class Annotation extends React.Component<IAnnotationProps> { render() { return DocListCast(this.props.anno.annotations).map(a => ( @@ -86,7 +86,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { } @action - onPointerDown = async (e: React.PointerEvent) => { + onPointerDown = (e: React.PointerEvent) => { if (e.button === 2 || e.ctrlKey) { PDFMenu.Instance.Status = "annotation"; PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this); @@ -97,11 +97,11 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> { e.stopPropagation(); } else if (e.button === 0) { - const annoGroup = await Cast(this.props.document.group, Doc); - if (annoGroup) { - DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation), false, undefined); - e.stopPropagation(); - } + e.persist(); + e.stopPropagation(); + PromiseValue(this.props.document.group).then(annoGroup => annoGroup instanceof Doc && + DocumentManager.Instance.FollowLink(undefined, annoGroup, (doc, followLinkLocation) => this.props.addDocTab(doc, e.ctrlKey ? "add" : followLinkLocation), false, undefined) + ); } } diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index 7bea8d01b..32dd376ac 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -1,17 +1,17 @@ import React = require("react"); -import "./PDFMenu.scss"; -import { observable, action, computed, } from "mobx"; -import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { unimplementedFunction, returnFalse, Utils } from "../../../Utils"; -import AntimodeMenu from "../AntimodeMenu"; -import { Doc, Opt } from "../../../fields/Doc"; +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; import { ColorState } from "react-color"; +import { Doc, Opt } from "../../../fields/Doc"; +import { returnFalse, unimplementedFunction, Utils } from "../../../Utils"; +import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu"; +import "./PDFMenu.scss"; @observer -export default class PDFMenu extends AntimodeMenu { +export class PDFMenu extends AntimodeMenu<AntimodeMenuProps> { static Instance: PDFMenu; private _commentCont = React.createRef<HTMLButtonElement>(); @@ -112,7 +112,7 @@ export default class PDFMenu extends AntimodeMenu { </div> </div>; return ( - <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} /> + <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} /> ); } @@ -154,7 +154,7 @@ export default class PDFMenu extends AntimodeMenu { const buttons = this.Status === "pdf" ? [ this.highlighter, - <button key="2" className="antimodeMenu-button" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown}> + <button key="2" className="antimodeMenu-button annotate" title="Drag to Annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: "grab" }}> <FontAwesomeIcon icon="comment-alt" size="lg" /></button>, ] : [ <button key="5" className="antimodeMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}> diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index f9ae78778..18be9b679 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,42 +1,43 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, FieldResult, HeightSym, Opt, WidthSym, AclAddonly, AclEdit, AclAdmin, DataSym } from "../../../fields/Doc"; +import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { InkTool } from "../../../fields/InkField"; import { List } from "../../../fields/List"; -import { createSchema, makeInterface, listSpec } from "../../../fields/Schema"; -import { ScriptField, ComputedField } from "../../../fields/ScriptField"; +import { createSchema, makeInterface } from "../../../fields/Schema"; +import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; import { PdfField } from "../../../fields/URLField"; -import { TraceMobx, GetEffectiveAcl } from "../../../fields/util"; +import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, emptyPath, intersectRect, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; +import { Networking } from "../../Network"; import { DragManager } from "../../util/DragManager"; import { CompiledScript, CompileScript } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionView } from "../collections/CollectionView"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; -import Annotation from "./Annotation"; -import PDFMenu from "./PDFMenu"; +import { Annotation } from "./Annotation"; +import { PDFMenu } from "./PDFMenu"; import "./PDFViewer.scss"; +const pdfjs = require('pdfjs-dist/es5/build/pdf.js'); import React = require("react"); -import { SnappingManager } from "../../util/SnappingManager"; +import { LinkDocPreview } from "../nodes/LinkDocPreview"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); const pdfjsLib = require("pdfjs-dist"); -import { Networking } from "../../Network"; export const pageSchema = createSchema({ - curPage: "number", + _curPage: "number", rotation: "number", scrollHeight: "number", serachMatch: "boolean" @@ -56,6 +57,7 @@ interface IViewerProps { Document: Doc; DataDoc?: Doc; docFilters: () => string[]; + searchFilterDocs: () => Doc[]; ContainingCollectionView: Opt<CollectionView>; PanelWidth: () => number; PanelHeight: () => number; @@ -97,16 +99,11 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @observable private _zoomed = 1; private _pdfViewer: any; + private _styleRule: any; // stylesheet rule for making hyperlinks clickable private _retries = 0; // number of times tried to create the PDF viewer private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - private _reactionDisposer?: IReactionDisposer; - private _selectionReactionDisposer?: IReactionDisposer; - private _annotationReactionDisposer?: IReactionDisposer; - private _scrollTopReactionDisposer?: IReactionDisposer; - private _filterReactionDisposer?: IReactionDisposer; - private _searchReactionDisposer?: IReactionDisposer; - private _searchReactionDisposer2?: IReactionDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewer: React.RefObject<HTMLDivElement> = React.createRef(); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _selectionText: string = ""; @@ -134,7 +131,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu const coreFilename = pathComponents.pop()!.split(".")[0]; const params: any = { coreFilename, - pageNum: this.Document.curPage || 1, + pageNum: this.Document._curPage || 1, }; if (pathComponents.length) { params.subtree = `${pathComponents.join("/")}/`; @@ -143,20 +140,20 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } else { const params: any = { coreFilename: relative.split("/")[relative.split("/").length - 1], - pageNum: this.Document.curPage || 1, + pageNum: this.Document._curPage || 1, }; this._coverPath = "http://cs.brown.edu/~bcz/face.gif";//href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" }; } runInAction(() => this._showWaiting = this._showCover = true); this.props.startupLive && this.setupPdfJsViewer(); this._mainCont.current && (this._mainCont.current.scrollTop = this.layoutDoc._scrollTop || 0); - this._searchReactionDisposer = reaction(() => this.Document.searchMatch, + this._disposers.searchMatch = reaction(() => Doc.IsSearchMatch(this.rootDoc), m => { - if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), true); + if (m) (this._lastSearch = true) && this.search(Doc.SearchQuery(), m.searchMatch > 0); else !(this._lastSearch = false) && setTimeout(() => !this._lastSearch && this.search("", false, true), 200); }, { fireImmediately: true }); - this._selectionReactionDisposer = reaction(() => this.props.isSelected(), + this._disposers.selected = reaction(() => this.props.isSelected(), selected => { if (!selected) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); @@ -166,26 +163,26 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(); }, { fireImmediately: true }); - this._reactionDisposer = reaction( + this._disposers.scrollY = reaction( () => this.Document._scrollY, (scrollY) => { if (scrollY !== undefined) { (this._showCover || this._showWaiting) && this.setupPdfJsViewer(); - this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); + (!LinkDocPreview.TargetDoc) && this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document._scrollY || 0)); setTimeout(() => this.Document._scrollY = undefined, 1000); } }, { fireImmediately: true } ); + this._disposers.curPage = reaction( + () => this.Document._curPage, + (page) => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), + { fireImmediately: true } + ); } componentWillUnmount = () => { - this._reactionDisposer?.(); - this._scrollTopReactionDisposer?.(); - this._annotationReactionDisposer?.(); - this._filterReactionDisposer?.(); - this._selectionReactionDisposer?.(); - this._searchReactionDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); document.removeEventListener("copy", this.copy); } @@ -226,11 +223,11 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this.props.setPdfViewer(this); await this.initialLoad(); - this._scrollTopReactionDisposer = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), + this._disposers.scrollTop = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null), (stop) => (stop !== undefined && this.layoutDoc._scrollY === undefined && this._mainCont.current) && (this._mainCont.current.scrollTop = stop), { fireImmediately: true }); - this._filterReactionDisposer = reaction( + this._disposers.filterScript = reaction( () => Cast(this.Document.filterScript, ScriptField), action(scriptField => { const oldScript = this._script.originalScript; @@ -245,8 +242,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } pagesinit = action(() => { - this._pdfViewer.currentScaleValue = this._zoomed = 1; - this.gotoPage(this.Document.curPage || 1); + if (this._pdfViewer._setDocumentViewerElement.offsetParent) { + this._pdfViewer.currentScaleValue = this._zoomed = 1; + this.gotoPage(this.Document._curPage || 1); + } document.removeEventListener("pagesinit", this.pagesinit); }); @@ -313,7 +312,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu annoDocs.push(annoDoc); anno.remove(); (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); - (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc.width), maxX)); + (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc._width), maxX)); })); mainAnnoDocProto.y = Math.max(minY, 0); @@ -342,7 +341,9 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action gotoPage = (p: number) => { - this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + if (this._pdfViewer?._setDocumentViewerElement?.offsetParent) { + this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + } } @action @@ -363,7 +364,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu @action onScroll = (e: React.UIEvent<HTMLElement>) => { this.Document._scrollY === undefined && (this.layoutDoc._scrollTop = this._mainCont.current!.scrollTop); - this._pdfViewer && (this.Document.curPage = this._pdfViewer.currentPageNumber); + this._pdfViewer && (this.Document._curPage = this._pdfViewer.currentPageNumber); } // get the page index that the vertical offset passed in is on @@ -436,11 +437,10 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; - addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); + (e.target as any).tagName === "SPAN" && (this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" })); if ((this.Document._viewScale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.active(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true); - //e.stopPropagation(); } this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active(true)) { @@ -462,12 +462,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._marqueeHeight = this._marqueeWidth = 0; this._marqueeing = true; } - document.removeEventListener("pointermove", this.onSelectMove); document.addEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointerup", this.removeStyle, true); } } + removeStyle = () => { + clearStyleSheetRules(PDFViewer._annotationStyle); + document.removeEventListener("pointerup", this.removeStyle); + } @action onSelectMove = (e: PointerEvent): void => { @@ -602,15 +605,15 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy](); // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() }); // Doc.GetProto(snipLayout).layout = PDFBox.LayoutString("snipped"); - const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection + const annotationDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color if (annotationDoc) { DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, { dragComplete: e => { - if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) { - const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); - annotationDoc.isLinkButton = true; - if (link) link.followLinkLocation = "onRight"; + if (!e.aborted && e.annoDragData && !e.annoDragData.linkDocument) { + e.annoDragData.linkDocument = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation"); + annotationDoc.isLinkButton = true; // prevents link button fro showing up --- maybe not a good thing? } + e.annoDragData && e.annoDragData.linkDocument && e.annoDragData?.linkDropCallback?.({ linkDocument: e.annoDragData.linkDocument }); } }); } diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx index a25a8ee33..c33637d0f 100644 --- a/src/client/views/presentationview/PresElementBox.tsx +++ b/src/client/views/presentationview/PresElementBox.tsx @@ -20,6 +20,7 @@ import { DocumentType } from "../../documents/DocumentTypes"; import { Tooltip } from "@material-ui/core"; import { DragManager } from "../../util/DragManager"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { undoBatch } from "../../util/UndoManager"; export const presSchema = createSchema({ presentationTargetDoc: Doc, @@ -102,6 +103,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc bringToFront={returnFalse} opacity={returnOne} docFilters={this.props.docFilters} + searchFilterDocs={this.props.searchFilterDocs} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} ContentScaling={returnOne} @@ -205,6 +207,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc } } + @undoBatch + removeItem = action((e: React.MouseEvent) => { + this.props.removeDocument?.(this.rootDoc); + if (PresBox.Instance._selectedArray.includes(this.rootDoc)) { + PresBox.Instance._selectedArray.splice(PresBox.Instance._selectedArray.indexOf(this.rootDoc), 1); + } + e.stopPropagation(); + }); + render() { const className = "presElementBox-item" + (PresBox.Instance._selectedArray.includes(this.rootDoc) ? " presElementBox-active" : ""); const pbi = "presElementBox-interaction"; @@ -253,10 +264,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc <Tooltip title={<><div className="dash-tooltip">{"Presentation pin view"}</div></>}><div className="presElementBox-time" style={{ fontWeight: 700, display: this.rootDoc.presPinView && PresBox.Instance.toolbarWidth > 300 ? "block" : "none" }}>V</div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div className="presElementBox-closeIcon" - onClick={e => { - this.props.removeDocument?.(this.rootDoc); - e.stopPropagation(); - }}> + onClick={this.removeItem}> <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} /> </div></Tooltip> <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"presElementBox-expand" + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}> diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 9b7cf2fc6..f1dd106a7 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -1,28 +1,11 @@ -import * as React from 'react'; +import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { observable, action } from 'mobx'; +import * as React from 'react'; +import { DocumentType } from "../../documents/DocumentTypes"; // import "./SearchBox.scss"; import "./IconBar.scss"; -import "./IconButton.scss"; -import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library } from '@fortawesome/fontawesome-svg-core'; -import * as _ from "lodash"; import { IconButton } from './IconButton'; -import { DocumentType } from "../../documents/DocumentTypes"; - - -library.add(faSearch); -library.add(faObjectGroup); -library.add(faImage); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); -library.add(faMusic); -library.add(faLink); -library.add(faChartBar); -library.add(faGlobeAsia); -library.add(faBan); +import "./IconButton.scss"; export interface IconBarProps { setIcons: (icons: string[]) => void; diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx index 52641c543..349690b20 100644 --- a/src/client/views/search/IconButton.tsx +++ b/src/client/views/search/IconButton.tsx @@ -1,30 +1,14 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { observable, action, runInAction, IReactionDisposer, reaction } from 'mobx'; -import "./SearchBox.scss"; -import "./IconButton.scss"; -import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { library, icon } from '@fortawesome/fontawesome-svg-core'; +import * as _ from "lodash"; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; import { DocumentType } from "../../documents/DocumentTypes"; import '../globalCssVariables.scss'; -import * as _ from "lodash"; import { IconBar } from './IconBar'; -import { props } from 'bluebird'; -import { Search } from '../../../server/Search'; -import { gravity } from 'sharp'; - -library.add(faSearch); -library.add(faObjectGroup); -library.add(faImage); -library.add(faStickyNote); -library.add(faFilePdf); -library.add(faFilm); -library.add(faMusic); -library.add(faLink); -library.add(faChartBar); -library.add(faGlobeAsia); -library.add(faBan); +import "./IconButton.scss"; +import "./SearchBox.scss"; +import { Font } from '@react-pdf/renderer'; interface IconButtonProps { type: string; @@ -47,59 +31,46 @@ export class IconButton extends React.Component<IconButtonProps>{ componentDidMount = () => { this._resetReaction = reaction( () => IconBar.Instance._resetClicked, - () => { + action(() => { if (IconBar.Instance._resetClicked) { - runInAction(() => { - this.reset(); - IconBar.Instance._reset++; - if (IconBar.Instance._reset === 9) { - IconBar.Instance._reset = 0; - IconBar.Instance._resetClicked = false; - } - }); + this._isSelected = false; + IconBar.Instance._reset++; + if (IconBar.Instance._reset === 9) { + IconBar.Instance._reset = 0; + IconBar.Instance._resetClicked = false; + } } - }, + }), ); + this._selectAllReaction = reaction( () => IconBar.Instance._selectAllClicked, - () => { + action(() => { if (IconBar.Instance._selectAllClicked) { - runInAction(() => { - this.select(); - IconBar.Instance._select++; - if (IconBar.Instance._select === 9) { - IconBar.Instance._select = 0; - IconBar.Instance._selectAllClicked = false; - } - }); + this._isSelected = true; + IconBar.Instance._select++; + if (IconBar.Instance._select === 9) { + IconBar.Instance._select = 0; + IconBar.Instance._selectAllClicked = false; + } } - }, + }), ); } @action.bound getIcon() { switch (this.props.type) { - case (DocumentType.NONE): - return faBan; - case (DocumentType.AUDIO): - return faMusic; - case (DocumentType.COL): - return faObjectGroup; - case (DocumentType.IMG): - return faImage; - case (DocumentType.LINK): - return faLink; - case (DocumentType.PDF): - return faFilePdf; - case (DocumentType.RTF): - return faStickyNote; - case (DocumentType.VID): - return faVideo; - case (DocumentType.WEB): - return faGlobeAsia; - default: - return faCaretDown; + case (DocumentType.NONE): return "ban"; + case (DocumentType.AUDIO): return "music"; + case (DocumentType.COL): return "object-group"; + case (DocumentType.IMG): return "image"; + case (DocumentType.LINK): return "link"; + case (DocumentType.PDF): return "file-pdf"; + case (DocumentType.RTF): return "sticky-note"; + case (DocumentType.VID): return "video"; + case (DocumentType.WEB): return "globe-asia"; + default: return "caret-down"; } } @@ -136,53 +107,16 @@ export class IconButton extends React.Component<IconButtonProps>{ //backgroundColor: "rgb(178, 206, 248)" //$darker-alt-accent }; - @action.bound - public reset() { this._isSelected = false; } - - @action.bound - public select() { this._isSelected = true; } - - @action - onMouseLeave = () => { this._hover = false; } - - @action - onMouseEnter = () => { this._hover = true; } - - getFA = () => { - switch (this.props.type) { - case (DocumentType.NONE): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faBan} />); - case (DocumentType.AUDIO): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faMusic} />); - case (DocumentType.COL): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faObjectGroup} />); - case (DocumentType.IMG): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faImage} />); - case (DocumentType.LINK): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faLink} />); - case (DocumentType.PDF): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faFilePdf} />); - case (DocumentType.RTF): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faStickyNote} />); - case (DocumentType.VID): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faVideo} />); - case (DocumentType.WEB): - return (<FontAwesomeIcon className="fontawesome-icon" icon={faGlobeAsia} />); - default: - return (<FontAwesomeIcon className="fontawesome-icon" icon={faCaretDown} />); - } - } - render() { return ( <div className="type-outer" id={this.props.type + "-filter"} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} + onMouseEnter={() => this._hover = true} + onMouseLeave={() => this._hover = false} onClick={this.onClick}> <div className="type-icon" id={this.props.type + "-icon"} style={this._hover ? this.hoverStyle : this._isSelected ? this.selected : this.notSelected} > - {this.getFA()} + <FontAwesomeIcon className="fontawesome-icon" icon={this.getIcon()} /> </div> {/* <div className="filter-description">{this.props.type}</div> */} </div> diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 3f06ba7d3..13d4a6d5a 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -6,71 +6,132 @@ flex-direction: column; width: 100%; height: 100%; - position: absolute; + position: relative; font-size: 10px; line-height: 1; overflow-y: auto; overflow-x: visible; - background: lightgrey, -} + background: lightgrey; + overflow: visible; + z-index: 10000; -.searchBox-bar { - height: 32px; - display: flex; - justify-content: center; - align-items: center; - background-color: black; - .searchBox-barChild { + .searchBox-bar { + height: $searchpanel-height; + display: flex; + justify-content: center; + align-items: center; + background-color: black; - &.searchBox-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px - } + .searchBox-lozenges { + position: absolute; + left: 15; + display: flex; - &.searchBox-input { - margin:5px; - border-radius:20px; - border:black; - display: block; - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - align-self: stretch; - outline:none; + .searchBox-lozenge-user, + .searchBox-lozenge-dashboard, + .searchBox-lozenge { + background-color: #313131; + border-radius: 5px; + height: 18px; + padding: 4px; + box-shadow: lightgrey 0.15em 0.15em 0.1em; + margin: 2px; + margin-bottom: 4px; + border-top: dimgrey 1px solid; + border-left: dimgrey 1px solid; + display: flex; + .searchBox-logoff, + .searchBox-dashboards { + border-radius: 3px; + background: olivedrab; + color: white; + position: relative; + display: none; + margin-left: 3px; + padding-left: 2px; + padding-right: 2px; + padding-bottom: 11px; + cursor: default; + } + .searchBox-logoff { + background: red; + } + + .searchBox-dashSelect{ + background-color: black; + color: white; + font-size: 9; + margin-right: 6; + border-radius: 5px; + position: relative; + height: 15px; + transform: translate(0,-3px); + + &:hover { + cursor: pointer; + } + } + } + .searchBox-lozenge-user:hover { + .searchBox-logoff { + display:inline-block; + } + } + .searchBox-lozenge-dashboard:hover { + .searchBox-dashboards { + display:inline-block; + } + } } - - .searchBox-input:focus { - width: 500px; - outline:none; + .searchBox-query { + position: relative; + display: flex; + width: 450; } + .searchBox-barChild { - &.searchBox-filter { - align-self: stretch; - button{ - transform:none; + &.searchBox-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px } - button:hover{ - transform:none; + + &.searchBox-input { + margin:5px; + border-radius:20px; + border:black; + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + align-self: stretch; + outline:none; + &:focus { + width: 500px; + outline:none; + } + } + &.searchBox-filter { + align-self: stretch; + button{ + transform:none; + &:hover { + transform: none; + } + } } - } - &.searchBox-submit { - margin-left: 2px; - margin-right: 2px - } + &.searchBox-submit { + margin-left: 2px; + margin-right: 2px + } - &.searchBox-close { - color: $light-color; - max-height: 32px; + &.searchBox-close { + color: $light-color; + max-height: $searchpanel-height; + } } - } -} - -.searchBox-quickFilter { - width: 100%; - height: 40px; - margin-top: 10px; + } } .searchBox-results { @@ -91,241 +152,4 @@ text-align: left; font-weight: bold; } -} - -.filter-form { - position: relative; - background: #121721; - flex-direction: column; - transform-origin: top; - transition: height 0.3s ease, display 0.6s ease, overflow 0.6s ease; - height:0px; - overflow:hidden; - - - .filter-header { - //display: flex; - position: relative; - //flex-wrap:wrap; - right: 1px; - color: grey; - //flex-direction: row-reverse; - transform-origin: top; - //justify-content: space-evenly; - margin-bottom: 5px; - overflow:hidden; - transition:height 0.3s ease-out; - - - - .filter-item { - position: relative; - border:1px solid grey; - border-radius: 16px; - - } - } - - .filter-body { - position: relative; - right: 1px; - color: grey; - transform-origin: top; - border-top: 0px; - - overflow:hidden; - transition:height 0.3s ease-out; - height:0px; - - } - .filter-key { - position: relative; - right: 1px; - color: grey; - transform-origin: top; - border-top: 0px; - overflow:hidden; - transition:height 0.3s ease-out; - height:0px; - - // .filter-keybar { - // display: flex; - // flex-wrap: wrap; - // justify-content: space-evenly; - // height: auto; - // width: 100%; - // flex-direction: row-reverse; - // margin-top:5px; - - // .filter-item { - // position: relative; - // border:1px solid grey; - // border-radius: 16px; - // } - // } - } -} - -// .top-filter-header { - -// #header { -// text-transform: uppercase; -// letter-spacing: 2px; -// font-size: 13; -// width: 80%; -// } - -// .close-icon { -// width: 20%; -// opacity: .6; -// position: relative; -// display: block; - -// .line { -// display: block; -// background: $alt-accent; -// width: 20; -// height: 3; -// position: absolute; -// right: 0; -// border-radius: ($height-line / 2); - -// &.line-1 { -// transform: rotate(45deg); -// top: 45%; -// } - -// &.line-2 { -// transform: rotate(-45deg); -// top: 45%; -// } -// } -// } - -// .close-icon:hover { -// opacity: 1; -// } - -// } - -// .filter-options { - -// .filter-div { -// margin-top: 10px; -// margin-bottom: 10px; -// display: inline-block; -// width: 100%; -// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent -// border-top-style: solid; - -// .filter-header { -// display: flex; -// align-items: center; -// margin-bottom: 10px; -// letter-spacing: 2px; - -// .filter-title { -// font-size: 13; -// text-transform: uppercase; -// margin-top: 10px; -// margin-bottom: 10px; -// -webkit-transition: all 0.2s ease-in-out; -// -moz-transition: all 0.2s ease-in-out; -// -o-transition: all 0.2s ease-in-out; -// transition: all 0.2s ease-in-out; -// } -// } - -// .filter-header:hover .filter-title { -// transform: scale(1.05); -// } - -// .filter-panel { -// max-height: 0px; -// width: 100%; -// overflow: hidden; -// opacity: 0; -// transform-origin: top; -// -webkit-transition: all 0.2s ease-in-out; -// -moz-transition: all 0.2s ease-in-out; -// -o-transition: all 0.2s ease-in-out; -// transition: all 0.2s ease-in-out; -// text-align: center; -// } -// } -// } - -// .filter-buttons { -// border-color: rgba(178, 206, 248, .2); // $darker-alt-accent -// border-top-style: solid; -// padding-top: 10px; -// } - - -.active-filters { - display: flex; - flex-direction: row-reverse; - justify-content: flex-end; - width: 100%; - margin-right: 30px; - position: relative; - - .active-icon { - max-width: 40px; - flex: initial; - - &.icon { - width: 40px; - text-align: center; - margin-bottom: 5px; - position: absolute; - } - - &.container { - display: flex; - flex-direction: column; - width: 40px; - } - - &.description { - text-align: center; - top: 40px; - position: absolute; - width: 40px; - font-size: 9px; - opacity: 0; - -webkit-transition: all 0.2s ease-in-out; - -moz-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - } - - &.icon:hover+.description { - opacity: 1; - } - } - - .col-icon { - height: 35px; - margin-left: 5px; - width: 35px; - background-color: black; - color: white; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - - .save-filter, - .reset-filter, - .all-filter { - background-color: gray; - } - - .save-filter:hover, - .reset-filter:hover, - .all-filter:hover { - background-color: $darker-alt-accent; - } - } }
\ No newline at end of file diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 770a03cb1..b37ae02c3 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,183 +1,99 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from '@material-ui/core'; -import { action, computed, observable, runInAction, reaction, IReactionDisposer } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as rp from 'request-promise'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; -import { Id } from '../../../fields/FieldSymbols'; +import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { returnFalse, Utils } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SetupDrag } from '../../util/DragManager'; import { SearchUtil } from '../../util/SearchUtil'; -import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { CollectionView, CollectionViewType } from '../collections/CollectionView'; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView, ColumnType } from "../collections/CollectionSchemaView"; +import { CollectionViewType } from '../collections/CollectionView'; import { ViewBoxBaseComponent } from "../DocComponent"; -import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import "./SearchBox.scss"; +import { undoBatch } from "../../util/UndoManager"; -export const searchSchema = createSchema({ - id: "string", - Document: Doc, - searchQuery: "string", -}); - -export enum Keys { - TITLE = "title", - AUTHOR = "author", - DATA = "data", - TEXT = "text" -} +export const searchSchema = createSchema({ Document: Doc }); type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; const SearchBoxDocument = makeInterface(documentSchema, searchSchema); -//React.Component<SearchProps> @observer export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDocument>(SearchBoxDocument) { + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + public static Instance: SearchBox; - get _searchString() { return this.layoutDoc.searchQuery; } - @computed set _searchString(value) { this.layoutDoc.searchQuery = (value); } - @observable private _resultsOpen: boolean = false; - @observable _searchbarOpen: boolean = false; - @observable private _results: [Doc, string[], string[]][] = []; - @observable private _openNoResults: boolean = false; - @observable private _visibleElements: JSX.Element[] = []; - @observable private _visibleDocuments: Doc[] = []; - - static NUM_SEARCH_RESULTS_PER_PAGE = 25; - - private _resultsSet = new Map<Doc, number>(); - private _resultsRef = React.createRef<HTMLDivElement>(); - public inputRef = React.createRef<HTMLInputElement>(); - - private _isSearch: ("search" | "placeholder" | undefined)[] = []; - private _isSorted: ("sorted" | "placeholder" | undefined)[] = []; - + private _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; + private _numResultsPerPage = 500; private _numTotalResults = -1; private _endIndex = -1; - - static Instance: SearchBox; - + private _lockPromise?: Promise<void>; + private _resultsSet = new Map<Doc, number>(); + private _inputRef = React.createRef<HTMLInputElement>(); private _maxSearchIndex: number = 0; private _curRequest?: Promise<any> = undefined; - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SearchBox, fieldKey); } + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.DOCHOLDER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - private new_buckets: { [characterName: string]: number } = {}; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _basicWordStatus: boolean = false; - @observable private _nodeStatus: boolean = false; - @observable private _keyStatus: boolean = false; + private docsforfilter: Doc[] | undefined = []; + private realTotalResults: number = 0; + private newsearchstring = ""; + private collectionRef = React.createRef<HTMLDivElement>(); - @observable private newAssign: boolean = true; + @observable _icons: string[] = this._allIcons; + @observable _results: [Doc, string[], string[]][] = []; + @observable _visibleElements: JSX.Element[] = []; + @observable _visibleDocuments: Doc[] = []; + @observable _deletedDocsStatus: boolean = false; + @observable _onlyAliases: boolean = true; + @observable _searchbarOpen = false; + @observable _searchFullDB = "DB"; + @observable _noResults = ""; + @observable _pageStart = 0; + @observable open = false; + @observable children = 0; + @computed get filter() { return this._results?.length && (this.currentSelectedCollection?.props.Document._searchFilterDocs || this.currentSelectedCollection?.props.Document._docFilters); } constructor(props: any) { super(props); SearchBox.Instance = this; - this.resultsScrolled = this.resultsScrolled.bind(this); - } - @observable setupButtons = false; - private _disposers: { [name: string]: IReactionDisposer } = {}; - - componentDidMount = () => { - this._disposers.filters = reaction(() => Cast(this.props.Document._docFilters, listSpec("string")), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks - newFilters => { - if (this.searchFullDB) { - runInAction(() => this._pageStart = 0); - this.submitSearch(); - // newFilters?.forEach(f => { - // console.log(f); - // }) - } - }); - if (this.setupButtons === false) { - runInAction(() => this.setupButtons = true); - } - if (this.inputRef.current) { - this.inputRef.current.focus(); - runInAction(() => { this._searchbarOpen = true; }); - } - if (this.rootDoc.searchQuery && this.newAssign) { - const sq = this.rootDoc.searchQuery; - runInAction(() => { - - // this._deletedDocsStatus=this.props.filterQuery!.deletedDocsStatus; - // this._authorFieldStatus=this.props.filterQuery!.authorFieldStatus - // this._titleFieldStatus=this.props.filterQuery!.titleFieldStatus; - // this._basicWordStatus=this.props.filterQuery!.basicWordStatus; - // this._icons=this.props.filterQuery!.icons; - this.newAssign = false; - }); - runInAction(() => { - this.layoutDoc._searchString = StrCast(sq); - this.submitSearch(); - }); + componentDidMount = action(() => { + if (this._inputRef.current) { + this._inputRef.current.focus(); } - } + this._disposers.filters = reaction(() => this.props.Document._docFilters, + (filters: any) => this.setSearchFilter(this.currentSelectedCollection, !this.filter ? undefined : this.docsforfilter)); + }); componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); } + @computed get currentSelectedCollection() { return CollectionDockingView.Instance; } - @action - getViews = (doc: Doc) => SearchUtil.GetViewsOfDocument(doc) - - - @observable newsearchstring: string = ""; - @action.bound - onChange(e: React.ChangeEvent<HTMLInputElement>) { - this.layoutDoc._searchString = e.target.value; + onChange = action((e: React.ChangeEvent<HTMLInputElement>) => { this.newsearchstring = e.target.value; if (e.target.value === "") { - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); + console.log("Reset start"); + this.docsforfilter = undefined; + this.setSearchFilter(this.currentSelectedCollection, undefined); + this.resetSearch(false); - if (this.currentSelectedCollection !== undefined) { - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection = undefined; - this.props.Document.selectedDoc = undefined; - - } - runInAction(() => { this.open = false; }); - this._openNoResults = false; + this.open = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -186,61 +102,16 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this._curRequest = undefined; this._maxSearchIndex = 0; } - } + }); - enter = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + enter = action((e: React.KeyboardEvent | undefined) => { + if (!e || e.key === "Enter") { this.layoutDoc._searchString = this.newsearchstring; - runInAction(() => this._pageStart = 0); - - if (StrCast(this.layoutDoc._searchString) !== "" || !this.searchFullDB) { - runInAction(() => this.open = true); - } - else { - runInAction(() => this.open = false); - - } + this._pageStart = 0; + this.open = StrCast(this.layoutDoc._searchString) !== "" || this._searchFullDB !== "DB"; this.submitSearch(); } - } - - @observable open: boolean = false; - - - public static async convertDataUri(imageUri: string, returnedFilename: string) { - try { - const posting = Utils.prepend("/uploadURI"); - const returnedUri = await rp.post(posting, { - body: { - uri: imageUri, - name: returnedFilename - }, - json: true, - }); - return returnedUri; - - } catch (e) { - console.log("SearchBox:" + e); - } - } - - public _allIcons: string[] = [DocumentType.INK, DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB]; - //if true, any keywords can be used. if false, all keywords are required. - //this also serves as an indicator if the word status filter is applied - @observable private _filterOpen: boolean = false; - //if icons = all icons, then no icon filter is applied - // get _icons() { return this.props.searchFileTypes; } - // set _icons(value) { - // this.props.setSearchFileTypes(value); - // } - @observable _icons: string[] = this._allIcons; - //if all of these are true, no key filter is applied - @observable private _titleFieldStatus: boolean = true; - @observable private _authorFieldStatus: boolean = true; - //this also serves as an indicator if the collection status filter is applied - @observable public _deletedDocsStatus: boolean = false; - @observable private _collectionStatus = false; - + }); getFinalQuery(query: string): string { //alters the query so it looks in the correct fields @@ -248,8 +119,6 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc //TODO: data const initialfilters = Cast(this.props.Document._docFilters, listSpec("string"), []); - const type: string[] = []; - const filters: string[] = []; for (let i = 0; i < initialfilters.length; i = i + 3) { @@ -277,9 +146,7 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[0].split(" "); - oldWords.forEach((word, i) => { - i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); - }); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); query = `(${query}) AND (${newWords.join(" ")})`; } else { @@ -287,194 +154,74 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const mod = "_t:"; const newWords: string[] = []; const oldWords = values[i].split(" "); - oldWords.forEach((word, i) => { - i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\""); - }); + oldWords.forEach((word, i) => i === 0 ? newWords.push(key + mod + "\"" + word + "\"") : newWords.push("AND " + key + mod + "\"" + word + "\"")); const v = "(" + newWords.join(" ") + ")"; if (i === 0) { - query = `(${query}) AND (${v}`; - if (values.length === 1) { - query = query + ")"; - } - } - else if (i === values.length - 1) { - query = query + " OR " + v + ")"; - } - else { - query = query + " OR " + v; + query = `(${query}) AND (${v}` + (values.length === 1 ? ")" : ""); } + else query = query + " OR " + v + (i === values.length - 1 ? ")" : ""); } } - } - - // let limit = typepos.length - // typepos.forEach(i => { - // if (i === 0) { - // if (i + 1 === limit) { - // query = query + " && " + filters[i] + "_t:" + filters; - // } - // else if (filters[i] === filters[i + 3]) { - // query = query + " && (" + filters[i] + "_t:" + filters; - // } - // else { - // query = query + " && " + filters[i] + "_t:" + filters; - // } - - // } - // else if (i + 3 > filters.length) { - - // } - // else { - - // } - - // }); - - // query = this.applyBasicFieldFilters(query); - - - - query = query.replace(/-\s+/g, ''); - query = query.replace(/-/g, ""); - return query; - } - - basicRequireWords(query: string): string { - return query.split(" ").join(" + ").replace(/ + /, ""); + return query.replace(/-\s+/g, ''); } @action filterDocsByType(docs: Doc[]) { const finalDocs: Doc[] = []; - const blockedTypes: string[] = [DocumentType.PRESELEMENT, DocumentType.DOCHOLDER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; docs.forEach(doc => { - const layoutresult = Cast(doc.type, "string"); - if (layoutresult && !blockedTypes.includes(layoutresult)) { - if (layoutresult && this._icons.includes(layoutresult)) { - finalDocs.push(doc); - } + const layoutresult = StrCast(doc.type, "string") as DocumentType; + if (layoutresult && !this._blockedTypes.includes(layoutresult) && this._icons.includes(layoutresult)) { + finalDocs.push(doc); } }); return finalDocs; } - addCollectionFilter(query: string): string { - const collections: Doc[] = this.getCurCollections(); - const oldWords = query.split(" "); - - const collectionString: string[] = []; - collections.forEach(doc => { - const proto = doc.proto; - const protoId = (proto || doc)[Id]; - const colString: string = "{!join from=data_l to=id}id:" + protoId + " "; - collectionString.push(colString); - }); - - let finalColString = collectionString.join(" "); - finalColString = finalColString.trim(); - return "+(" + finalColString + ")" + query; - } - - get filterTypes() { - return this._icons.length === this._allIcons.length ? undefined : this._icons; - } - - //TODO: basically all of this - //gets all of the collections of all the docviews that are selected - //if a collection is the only thing selected, search only in that collection (not its container) - getCurCollections(): Doc[] { - const selectedDocs: DocumentView[] = SelectionManager.SelectedDocuments(); - const collections: Doc[] = []; - selectedDocs.forEach(async element => { - const layout: string = StrCast(element.props.Document.layout); - //checks if selected view (element) is a collection. if it is, adds to list to search through - if (layout.indexOf("Collection") > -1) { - //makes sure collections aren't added more than once - if (!collections.includes(element.props.Document)) { - collections.push(element.props.Document); - } - } - //makes sure collections aren't added more than once - if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) { - collections.push(element.props.ContainingCollectionDoc); - } - }); - - return collections; + static foreachRecursiveDoc(docs: Doc[], func: (doc: Doc) => void) { + let newarray: Doc[] = []; + while (docs.length > 0) { + newarray = []; + docs.forEach(d => { + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + data && newarray.push(...DocListCast(data)); + func(d); + }); + docs = newarray; + } } - - currentSelectedCollection: DocumentView | undefined = undefined; - docsforfilter: Doc[] = []; - + @action searchCollection(query: string) { - const selectedCollection: DocumentView = SelectionManager.SelectedDocuments()[0]; + const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; query = query.toLowerCase(); if (selectedCollection !== undefined) { - this.currentSelectedCollection = selectedCollection; - if (this.filter === true) { - this.props.Document.selectedDoc = selectedCollection.props.Document; - } - let docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); + // this._currentSelectedCollection = selectedCollection; + const docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); const found: [Doc, string[], string[]][] = []; - const docsforFilter: Doc[] = []; - let newarray: Doc[] = []; + SearchBox.foreachRecursiveDoc(docs, (doc: Doc) => { + const hlights = new Set<string>(); + SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key)); + Array.from(hlights.keys()).length > 0 && found.push([doc, Array.from(hlights.keys()), []]); + }); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - d ? console.log(Cast(d.context, Doc)) : null; - if (d.data !== undefined) { - newarray.push(...DocListCast(d.data)); - } - const hlights: string[] = []; - const protos = Doc.GetAllPrototypes(d); - protos.forEach(proto => { - Object.keys(proto).forEach(key => { - if (StrCast(d[key]).toLowerCase().includes(query) && !hlights.includes(key)) { - hlights.push(key); - } - }); - }); - if (hlights.length > 0) { - found.push([d, hlights, []]); - docsforFilter.push(d); - } - }); - docs = newarray; - } this._results = found; - this.docsforfilter = docsforFilter; - if (this.filter === true) { - selectedCollection.props.Document._searchDocs = new List<Doc>(docsforFilter); - docs = DocListCast(selectedCollection.dataDoc[Doc.LayoutFieldKey(selectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(docsforFilter); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - } + this.docsforfilter = this._results.map(r => r[0]); + this.setSearchFilter(selectedCollection, this.filter && found.length ? this.docsforfilter : undefined); this._numTotalResults = found.length; + this.realTotalResults = found.length; } else { - this.noresults = "No collection selected :("; + this._noResults = "No collection selected :("; } } - - documentKeys(doc: Doc) { + static documentKeys(doc: Doc) { const keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be @@ -482,151 +229,72 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu // is displayed (unlikely) it won't show up until something else changes. //TODO Types - Doc.GetAllPrototypes(doc).map - (proto => Object.keys(proto).forEach(key => keys[key] = false)); + Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)); return Array.from(Object.keys(keys)); } - applyBasicFieldFilters(query: string) { - let finalQuery = ""; - - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TITLE); - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.AUTHOR); - - if (this._deletedDocsStatus) { - finalQuery = finalQuery + this.basicFieldFilters(query, Keys.TEXT); - } - return finalQuery; - } - - basicFieldFilters(query: string, type: string): string { - let mod = ""; - switch (type) { - case Keys.AUTHOR: mod = " author_t:"; break; - case Keys.TITLE: mod = " title_t:"; break; - case Keys.TEXT: mod = " text_t:"; break; - } - - const newWords: string[] = []; - const oldWords = query.split(" "); - oldWords.forEach(word => newWords.push(mod + word)); - - query = newWords.join(" "); - - return query; - } - - get fieldFiltersApplied() { return !(this._authorFieldStatus && this._titleFieldStatus); } - @action - submitSearch = async (reset?: boolean) => { - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - //d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } + submitSearch = async () => { + this.resetSearch(false); - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - if (reset) { - this.layoutDoc._searchString = ""; - } //this.props.Document._docFilters = new List(); - this.noresults = ""; + this._noResults = ""; this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.headercount = 0; this.children = 0; - this.buckets = []; - this.new_buckets = {}; let query = StrCast(this.layoutDoc._searchString); Doc.SetSearchQuery(query); - this.searchFullDB ? query = this.getFinalQuery(query) : console.log("local"); - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - result[0].searchMatch = undefined; - }); + this._searchFullDB && (query = this.getFinalQuery(query)); this._results = []; this._resultsSet.clear(); - this._isSearch = []; - this._isSorted = []; this._visibleElements = []; this._visibleDocuments = []; - if (StrCast(this.props.Document.searchQuery)) { - if (this._timeout) { clearTimeout(this._timeout); this._timeout = undefined; } - this._timeout = setTimeout(() => { - console.log("Resubmitting search"); - }, 60000); - } - if (query !== "") { + if (query || this._searchFullDB === "My Stuff") { this._endIndex = 12; this._maxSearchIndex = 0; this._numTotalResults = -1; - this.searchFullDB ? await this.getResults(query) : this.searchCollection(query); + this._searchFullDB ? await this.searchDatabase(query) : this.searchCollection(query); runInAction(() => { - this._resultsOpen = true; - this._searchbarOpen = true; - this._openNoResults = true; + this.open = this._searchbarOpen = true; this.resultsScrolled(); - }); } } - @observable searchFullDB = true; - - @observable _timeout: any = undefined; - - @observable firststring: string = ""; - @observable secondstring: string = ""; - - @observable bucketcount: number[] = []; - @observable buckets: Doc[] | undefined; - getAllResults = async (query: string) => { return SearchUtil.Search(query, true, { fq: this.filterQuery, start: 0, rows: 10000000 }); } private get filterQuery() { - const types = ["preselement", "docholder", "search", "searchitem", "script", "fonticonbox", "button", "label"]; // this.filterTypes; - const baseExpr = "NOT baseProto_b:true AND NOT system_b:true"; - const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true"; - const includeIcons = this.getDataStatus() ? "" : " NOT type_t:fonticonbox"; - const typeExpr = !types ? "" : ` ${types.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; + const baseExpr = "NOT system_b:true"; + const authorExpr = this._searchFullDB === "My Stuff" ? ` author_t:${Doc.CurrentUserEmail}` : undefined; + const includeDeleted = this._deletedDocsStatus ? "" : " NOT deleted_b:true"; + const typeExpr = this._onlyAliases ? "NOT {!join from=id to=proto_i}type_t:*" : `(type_t:* OR {!join from=id to=proto_i}type_t:*) ${this._blockedTypes.map(type => `NOT ({!join from=id to=proto_i}type_t:${type}) AND NOT type_t:${type}`).join(" AND ")}`; // fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello - const query = [baseExpr, includeDeleted, includeIcons, typeExpr].join(" AND ").replace(/AND $/, ""); - return query; + return [baseExpr, authorExpr, includeDeleted, typeExpr].filter(q => q).join(" AND ").replace(/AND $/, ""); } - getDataStatus() { return this._deletedDocsStatus; } + @computed get primarySort() { + const suffixMap = (type: ColumnType) => { + switch (type) { + case ColumnType.Date: return "_d"; + case ColumnType.String: return "_t"; + case ColumnType.Boolean: return "_b"; + case ColumnType.Number: return "_n"; + } + }; + const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); + return headers.reduce((p: Opt<string>, header: SchemaHeaderField) => p || (header.desc !== undefined && suffixMap(header.type) ? (header.heading + suffixMap(header.type) + (header.desc ? " desc" : " asc")) : undefined), undefined); + } - private NumResults = 50; - private lockPromise?: Promise<void>; - getResults = async (query: string) => { - if (this.lockPromise) { - await this.lockPromise; - } - this.lockPromise = new Promise(async res => { + searchDatabase = async (query: string) => { + this._lockPromise && (await this._lockPromise); + this._lockPromise = new Promise(async res => { while (this._results.length <= this._endIndex && (this._numTotalResults === -1 || this._maxSearchIndex < this._numTotalResults)) { - this._curRequest = SearchUtil.Search(query, true, { fq: this.filterQuery, start: this._maxSearchIndex, rows: this.NumResults, hl: true, "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { + this._curRequest = SearchUtil.Search(query, true, { onlyAliases: true, allowAliases: true, /*sort: this.primarySort,*/ fq: this.filterQuery, start: 0, rows: this._numResultsPerPage, hl: "on", "hl.fl": "*", }).then(action(async (res: SearchUtil.DocSearchResult) => { // happens at the beginning - this.realTotalResults = res.numFound; + this.realTotalResults = res.numFound <= 0 ? 0 : res.numFound; if (res.numFound !== this._numTotalResults && this._numTotalResults === -1) { this._numTotalResults = res.numFound; } @@ -634,55 +302,49 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc const highlightList = res.docs.map(doc => highlighting[doc[Id]]); const lines = new Map<string, string[]>(); res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i])); - const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc)); + const docs = res.docs; const highlights: typeof res.highlighting = {}; docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]); const filteredDocs = this.filterDocsByType(docs); - runInAction(() => { - filteredDocs.forEach((doc, i) => { - const index = this._resultsSet.get(doc); - const highlight = highlights[doc[Id]]; - const line = lines.get(doc[Id]) || []; - const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; - // if (this.findCommonElements(hlights)) { - // } - if (index === undefined) { - this._resultsSet.set(doc, this._results.length); - this._results.push([doc, hlights, line]); - } else { - this._results[index][1].push(...hlights); - this._results[index][2].push(...line); - } + runInAction(() => filteredDocs.forEach((doc, i) => { + const index = this._resultsSet.get(doc); + const highlight = highlights[doc[Id]]; + const line = lines.get(doc[Id]) || []; + const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)).filter(k => k) : []; + // if (this.findCommonElements(hlights)) { + // } + if (index === undefined) { + this._resultsSet.set(doc, this._results.length); + this._results.push([doc, hlights, line]); + } else { + this._results[index][1].push(...hlights); + this._results[index][2].push(...line); + } - }); - }); + })); this._curRequest = undefined; })); - this._maxSearchIndex += this.NumResults; + this._maxSearchIndex += this._numResultsPerPage; await this._curRequest; } this.resultsScrolled(); + + const selectedCollection = this.currentSelectedCollection;//SelectionManager.SelectedDocuments()[0]; + this.docsforfilter = this._results.map(r => r[0]); + this.setSearchFilter(selectedCollection, this.filter ? this.docsforfilter : undefined); res(); }); - return this.lockPromise; + return this._lockPromise; } - @observable noresults = ""; - collectionRef = React.createRef<HTMLSpanElement>(); + startDragCollection = async () => { const res = await this.getAllResults(this.getFinalQuery(StrCast(this.layoutDoc._searchString))); const filtered = this.filterDocsByType(res.docs); - const docs = filtered.map(doc => { - const isProto = Doc.GetT(doc, "isPrototype", "boolean", true); - if (isProto) { - return Doc.MakeDelegate(doc); - } else { - return Doc.MakeAlias(doc); - } - }); + const docs = filtered.map(doc => Doc.GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeDelegate(doc) : Doc.MakeAlias(doc)); let x = 0; let y = 0; for (const doc of docs.map(d => Doc.Layout(d))) { @@ -706,28 +368,26 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc y += 300; } } - return Docs.Create.SchemaDocument(Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []), DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); + const headers = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).map(h => { const v = h[Copy](); v.color = "#f1efeb"; return v; }); + return Docs.Create.SchemaDocument(headers, DocListCast(this.dataDoc[this.fieldKey]), { _autoHeight: true, _viewType: CollectionViewType.Schema, title: StrCast(this.layoutDoc._searchString) }); } @action.bound openSearch(e: React.SyntheticEvent) { e.stopPropagation(); - this._openNoResults = false; - this._resultsOpen = true; - this._searchbarOpen = true; + this._results.forEach(result => Doc.BrushDoc(result[0])); } - realTotalResults: number = 0; - - @action.bound - closeSearch = () => { - //this.closeResults(); - this._searchbarOpen = false; - } + resetSearch = action((close: boolean) => { + this._results.forEach(result => { + Doc.UnBrushDoc(result[0]); + Doc.ClearSearchMatches(); + }); + close && (this.open = this._searchbarOpen = false); + }); @action.bound closeResults() { - this._resultsOpen = false; this._results = []; this._resultsSet.clear(); this._visibleElements = []; @@ -737,21 +397,10 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc this._curRequest = undefined; } - @observable _pageStart: number = 0; - @observable _pageCount: number = SearchBox.NUM_SEARCH_RESULTS_PER_PAGE; - - @observable children: number = 0; @action resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => { - if (!this._resultsRef.current) return; this._endIndex = 30; const headers = new Set<string>(["title", "author", "text", "type", "data", "*lastModified", "context"]); - // if ((this._numTotalResults === 0 || this._results.length === 0) && this._openNoResults) { - // if (this.noresults === "") { - // this.noresults = "No search results :("; - // } - // return; - // } if (this._numTotalResults <= this._maxSearchIndex) { this._numTotalResults = this._results.length; @@ -763,301 +412,170 @@ export class SearchBox extends ViewBoxBaseComponent<FieldViewProps, SearchBoxDoc // undefined until a searchitem is put in there this._visibleElements = Array<JSX.Element>(this._numTotalResults === -1 ? 0 : this._numTotalResults); this._visibleDocuments = Array<Doc>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - // indicates if things are placeholders - this._isSearch = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - this._isSorted = Array<undefined>(this._numTotalResults === -1 ? 0 : this._numTotalResults); - } - let max = this.NumResults; + let max = this._numResultsPerPage; max > this._results.length ? max = this._results.length : console.log(""); for (let i = this._pageStart; i < max; i++) { //if the index is out of the window then put a placeholder in //should ones that have already been found get set to placeholders? - if (this._isSearch[i] !== "search") { - let result: [Doc, string[], string[]] | undefined = undefined; - - result = this._results[i]; - if (result) { - const highlights = Array.from([...Array.from(new Set(result[1]).values())]); - const lines = new List<string>(result[2]); - highlights.forEach((item) => headers.add(item)); - result[0].lines = lines; - result[0].highlighting = highlights.join(", "); - result[0].searchMatch = true; - if (i < this._visibleDocuments.length) { - this._visibleDocuments[i] = result[0]; - this._isSearch[i] = "search"; - Doc.BrushDoc(result[0]); - Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); - this.children++; - } - + let result: [Doc, string[], string[]] | undefined = undefined; + + result = this._results[i]; + if (result) { + const highlights = Array.from([...Array.from(new Set(result[1]).values())]); + const lines = new List<string>(result[2]); + highlights.forEach((item) => headers.add(item)); + Doc.SetSearchMatch(result[0], { searchMatch: 1 }); + if (i < this._visibleDocuments.length) { + this._visibleDocuments[i] = result[0]; + Doc.BrushDoc(result[0]); + Doc.AddDocToList(this.dataDoc, this.props.fieldKey, result[0]); + this.children++; } - } } - this.headerscale = headers.size; - if (Cast(this.props.Document._docFilters, listSpec("string"), []).length === 0) { - const oldSchemaHeaders = Cast(this.props.Document._schemaHeaders, listSpec("string"), []); - if (oldSchemaHeaders?.length && typeof oldSchemaHeaders[0] !== "object") { - const newSchemaHeaders = oldSchemaHeaders.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - headers.forEach(header => { - if (oldSchemaHeaders.includes(header) === false) { - newSchemaHeaders.push(new SchemaHeaderField(header, "#f1efeb")); - } - }); - this.headercount = newSchemaHeaders.length; - this.props.Document._schemaHeaders = new List<SchemaHeaderField>(newSchemaHeaders); - } else if (this.props.Document._schemaHeaders === undefined) { - this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); - } + if (this.props.Document._schemaHeaders === undefined) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); } if (this._maxSearchIndex >= this._numTotalResults) { this._visibleElements.length = this._results.length; this._visibleDocuments.length = this._results.length; - this._isSearch.length = this._results.length; } } - @observable headercount: number = 0; - @observable headerscale: number = 0; - - findCommonElements(arr2: string[]) { - const arr1 = ["layout", "data"]; - return arr1.some(item => arr2.includes(item)); - } - - @computed - get resFull() { return this._numTotalResults <= 8; } - - @computed - get resultHeight() { return this._numTotalResults * 70; } - - addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc); - moveButtonDoc = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc); - @computed get searchItemTemplate() { return Cast(Doc.UserDoc().searchItemTemplate, Doc, null); } + getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight + panelHeight = () => this.props.PanelHeight(); + selectElement = (doc: Doc) => { /* this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); */ }; + returnHeight = () => NumCast(this.layoutDoc._height); + returnLength = () => Math.min(window.innerWidth, 51 + 205 * Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length); - @computed get viewspec() { return Cast(this.props.Document._docFilters, listSpec("string"), []); } - - getTransform = () => { - return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight - } - panelHeight = () => { - return this.props.PanelHeight(); - } - selectElement = (doc: Doc) => { - //this.gotoDocument(this.childDocs.indexOf(doc), NumCasst(this.layoutDoc._itemIndex)); - } - - addDocument = (doc: Doc) => { - return null; + @action + changeSearchScope = (scope: string) => { + this.docsforfilter = undefined; + this.setSearchFilter(this.currentSelectedCollection, undefined); + this._searchFullDB = scope; + this.dataDoc[this.fieldKey] = new List<Doc>([]); + this.submitSearch(); + } + + @computed get scopeButtons() { + return <div style={{ height: 25, paddingLeft: "4px", paddingRight: "4px", border: "1px solid gray", borderRadius: "0.3em", borderBottom: !this.open ? "1px solid" : "none", }}> + <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> + <div style={{ display: "contents" }}> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this._searchFullDB} onChange={() => this.changeSearchScope("")} /> + Dashboard + </label> + </div> + <div className="radio" style={{ margin: 0 }}> + <label style={{ fontSize: 12, marginTop: 6 }} > + <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={this._searchFullDB?.length ? true : false} onChange={() => this.changeSearchScope("DB")} /> + DB + <span onClick={action(() => this._searchFullDB = this._searchFullDB === "My Stuff" ? "DB" : "My Stuff")}> + {this._searchFullDB === "My Stuff" ? "(me)" : "(full)"} + </span> + </label> + </div> + </div> + </form> + </div>; } - @observable filter = false; + setSearchFilter = action((collectionView: { props: { Document: Doc } }, docsForFilter: Doc[] | undefined) => { + if (collectionView) { + const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), null); + collectionView.props.Document._searchFilterDocs = docsForFilter?.length ? new List<Doc>(docsForFilter) : undefined; + collectionView.props.Document._docFilters = docsForFilter?.length && docFilters?.length ? new List<string>(docFilters) : undefined; + } + }); - @action newpage() { - this._pageStart += SearchBox.NUM_SEARCH_RESULTS_PER_PAGE; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.resultsScrolled(); - } render() { - this.props.Document._chromeStatus === "disabled"; - this.props.Document._searchDoc = true; - const cols = Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []).length; - let length = 0; - length = cols * 205 + 51; - let height = 0; - const rows = this.children; - height = 31 + 31 * 6; + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> <div className="searchBox-bar"> - <div style={{ position: "absolute", left: 15 }}>{Doc.CurrentUserEmail}</div> - <div style={{ display: "flex", alignItems: "center" }}> - <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>} ><div> - <FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" - style={{ cursor: "hand", color: "black", padding: 1, left: 35, position: "relative" }} /> - </div></Tooltip> - <div style={{ - position: "relative", - left: 245, - zIndex: 9000, - color: "grey", - background: "white", - }}> {`${this._results.length}` + " of " + `${this.realTotalResults}`}</div> - <div style={{ cursor: "default", left: 250, position: "relative", }}> - <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} ><div> - <FontAwesomeIcon icon={"filter"} size="lg" - style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} - onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined); }} - onClick={action(() => { - ///DONT Change without emailing andy r first. - this.filter = !this.filter && !this.searchFullDB; - if (this.filter === true && this.currentSelectedCollection !== undefined) { - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>(this.docsforfilter); - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(this.docsforfilter); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._docFilters = new List<string>(this.viewspec); - this.props.Document.selectedDoc = this.currentSelectedCollection.props.Document; - } - else if (this.filter === false && this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - - this.currentSelectedCollection.props.Document._searchDocs = new List<Doc>([]); - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.props.Document.selectedDoc = undefined; - } - } - )} /> - </div></Tooltip></div> - <input value={this.newsearchstring} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef} - className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch} - style={{ padding: 1, paddingLeft: 20, paddingRight: 20, color: "black", height: 20, width: 250 }} /> - <div style={{ - height: 25, - paddingLeft: "4px", - paddingRight: "4px", - border: "1px solid gray", - borderRadius: "0.3em", - borderBottom: this.open === false ? "1px solid" : "none", - }}> - <form className="beta" style={{ justifyContent: "space-evenly", display: "flex" }}> - <div style={{ display: "contents" }}> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input type="radio" style={{ marginLeft: -16, marginTop: -1 }} checked={!this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - Collection - </label> - </div> - <div className="radio" style={{ margin: 0 }}> - <label style={{ fontSize: 12, marginTop: 6 }} > - <input style={{ marginLeft: -16, marginTop: -1 }} type="radio" checked={this.searchFullDB} onChange={() => { - runInAction(() => { - this.searchFullDB = !this.searchFullDB; - this.dataDoc[this.fieldKey] = new List<Doc>([]); - this.filter = false; - if (this.currentSelectedCollection !== undefined) { - let newarray: Doc[] = []; - let docs: Doc[] = []; - docs = DocListCast(this.currentSelectedCollection.dataDoc[Doc.LayoutFieldKey(this.currentSelectedCollection.dataDoc)]); - while (docs.length > 0) { - newarray = []; - docs.forEach((d) => { - if (d.data !== undefined) { - d._searchDocs = new List<Doc>(); - d._docFilters = new List(); - const newdocs = DocListCast(d.data); - newdocs.forEach((newdoc) => { - newarray.push(newdoc); - }); - } - }); - docs = newarray; - } - this.currentSelectedCollection.props.Document._docFilters = new List(); - this.currentSelectedCollection.props.Document._searchDocs = undefined; - this.currentSelectedCollection = undefined; - } - this.submitSearch(); - }); - }} /> - DB - </label> - </div> - </div> - </form> + <div className="searchBox-lozenges" > + <div className="searchBox-lozenge-user"> + {`${Doc.CurrentUserEmail}`} + <div className="searchBox-logoff" onClick={() => window.location.assign(Utils.prepend("/logout"))}> + Logoff + </div> + </div> + <div className="searchBox-lozenge"> + {`UI project`} + </div> + <div className="searchBox-lozenge-dashboard" > + <select className="searchBox-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])} + value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}> + {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)} + </select> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}> + New + </div> + <div className="searchBox-dashboards" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}> + Snapshot + </div> </div> </div> - - </div> - <div style={{ zIndex: 20000, color: "black" }}> - {this._searchbarOpen === true ? + <div className="searchBox-query" > + <input defaultValue={""} autoComplete="off" onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this._inputRef} + className="searchBox-barChild searchBox-input" onKeyPress={this.enter} + style={{ padding: 1, paddingLeft: 20, paddingRight: 60, color: "black", height: 20, width: 250 }} /> + <div style={{ display: "flex", alignItems: "center" }}> + <div style={{ position: "absolute", left: 10 }}> + <Tooltip title={<div className="dash-tooltip" >drag search results as collection</div>}> + <div ref={this.collectionRef}><FontAwesomeIcon onPointerDown={SetupDrag(this.collectionRef, () => StrCast(this.layoutDoc._searchString) ? this.startDragCollection() : undefined)} icon={"search"} size="lg" + style={{ cursor: "hand", color: "black", padding: 1, position: "relative" }} /></div> + </Tooltip> + </div> + <div style={{ position: "absolute", left: 200, width: 30, zIndex: 9000, color: "grey", background: "white", }}> + {`${this._results.length}` + " of " + `${this.realTotalResults}`} + </div> + <div style={{ cursor: "default", left: 235, position: "absolute", }}> + <Tooltip title={<div className="dash-tooltip" >only display documents matching search</div>} > + <div> + <FontAwesomeIcon icon={"filter"} size="lg" + style={{ cursor: "hand", padding: 1, backgroundColor: this.filter ? "white" : "lightgray", color: this.filter ? "black" : "white" }} + onPointerDown={e => { e.stopPropagation(); SetupDrag(this.collectionRef, () => this.layoutDoc._searchString ? this.startDragCollection() : undefined); }} + onClick={action(() => this.setSearchFilter(this.currentSelectedCollection, this.filter ? undefined : this.docsforfilter))} /> + </div> + </Tooltip> + </div> + {this.scopeButtons} + </div> + </div > + </div > + {!this._searchbarOpen ? (null) : + <div style={{ zIndex: 20000, color: "black" }} ref={(r) => r?.focus()}> <div style={{ display: "flex", justifyContent: "center", }}> - {this.noresults === "" ? <div style={{ display: this.open === true ? "flex" : "none", overflow: "auto", }}> - <CollectionView {...this.props} + <div style={{ display: this.open ? "flex" : "none", overflow: "auto", position: "absolute" }}> + <CollectionSchemaView {...this.props} + CollectionView={undefined} + annotationsKey={""} + addDocument={returnFalse} Document={this.props.Document} moveDocument={returnFalse} removeDocument={returnFalse} - PanelHeight={this.open === true ? () => height : () => 0} - PanelWidth={this.open === true ? () => length : () => 0} - overflow={length > window.innerWidth || rows > 6 ? true : false} + PanelHeight={this.open ? this.returnHeight : returnZero} + PanelWidth={this.open ? this.returnLength : returnZero} + overflow={length > window.innerWidth || this.children > 6 ? true : false} focus={this.selectElement} ScreenToLocalTransform={Transform.Identity} /> - </div> : - <div style={{ display: "flex", justifyContent: "center" }}><div style={{ height: 200, top: 54, minWidth: 400, position: "absolute", backgroundColor: "rgb(241, 239, 235)", display: "flex", justifyContent: "center", alignItems: "center", border: "black 1px solid", }}> - <div>{this.noresults}</div> - </div></div>} - </div> : undefined} - </div> - - <div className="searchBox-results" onScroll={this.resultsScrolled} style={{ - display: this._resultsOpen ? "flex" : "none", - height: this.resFull ? "auto" : this.resultHeight, - overflow: "visibile" // this.resFull ? "auto" : "visible" - }} ref={this._resultsRef}> - </div> + <div style={{ position: "absolute", right: 5, bottom: 7, width: 15, height: 15, }} + onPointerDown={e => setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + this.props.Document._height = NumCast(this.props.Document._height) + delta[1]; + return false; + }, returnFalse, emptyFunction)} + > + <FontAwesomeIcon icon="grip-lines" size="lg" /> + </div> + </div> + </div> + </div> + } </div > ); } diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 647e1ce6f..82c0e19c8 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -1,20 +1,16 @@ -import { observer } from "mobx-react"; -import React = require("react"); -import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView"; -import { FieldViewProps, FieldView } from "../nodes/FieldView"; -import { observable, action } from "mobx"; -import { DocumentDecorations } from "../DocumentDecorations"; -import "../../views/nodes/WebBox.scss"; -import "./DashWebRTCVideo.scss"; -import { initialize, hangup, refreshVideos } from "./WebCamLogic"; +import { faPhoneSlash, faSync } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons"; +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; import { Doc } from "../../../fields/Doc"; import { InkTool } from "../../../fields/InkField"; - -library.add(faSync); -library.add(faPhoneSlash); +import "../../views/nodes/WebBox.scss"; +import { DocumentDecorations } from "../DocumentDecorations"; +import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import "./DashWebRTCVideo.scss"; +import { hangup, initialize, refreshVideos } from "./WebCamLogic"; +import React = require("react"); /** |
