diff options
33 files changed, 510 insertions, 986 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index f22df0da2..ef9c51b8b 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -482,31 +482,20 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start; }; -export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, finish?: () => void, reset?: { resetGoTo: { to: number, duration: number } | undefined }) { +export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number) { const elements = (element instanceof HTMLElement ? [element] : element); - let starts = elements.map(element => element.scrollTop); - let startDate = new Date().getTime(); + const starts = elements.map(element => element.scrollTop); + const startDate = new Date().getTime(); const animateScroll = () => { const currentDate = new Date().getTime(); - let currentTime = currentDate - startDate; - const resetParams = reset?.resetGoTo; - if (resetParams) { - reset!.resetGoTo = undefined; - const { to: newTo, duration: newDuration } = resetParams; - to = newTo; - starts = starts.map(start => easeInOutQuad(currentTime, start, to - start, duration)); - startDate = currentDate; - duration = newDuration; - currentTime = currentDate - startDate; - } + const currentTime = currentDate - startDate; elements.map((element, i) => element.scrollTop = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)); if (currentTime < duration) { requestAnimationFrame(animateScroll); } else { elements.forEach(element => element.scrollTop = to); - finish?.(); } }; animateScroll(); @@ -609,6 +598,44 @@ export function lightOrDark(color: any) { } } + +export function getWordAtPoint(elem: any, x: number, y: number): string | undefined { + if (elem.nodeType === elem.TEXT_NODE) { + const range = elem.ownerDocument.createRange(); + range.selectNodeContents(elem); + var currentPos = 0; + const endPos = range.endOffset; + while (currentPos + 1 < endPos) { + range.setStart(elem, currentPos); + range.setEnd(elem, currentPos + 1); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.expand?.("word"); // doesn't exist in firefox + const ret = range.toString(); + range.detach(); + return (ret); + } + currentPos += 1; + } + } else { + for (const childNode of elem.childNodes) { + const range = childNode.ownerDocument.createRange(); + range.selectNodeContents(childNode); + const rangeRect = range.getBoundingClientRect(); + if (rangeRect.left <= x && rangeRect.right >= x && + rangeRect.top <= y && rangeRect.bottom >= y) { + range.detach(); + const word = getWordAtPoint(childNode, x, y); + if (word) return word; + } else { + range.detach(); + } + } + } + return undefined; +} + export function hasDescendantTarget(x: number, y: number, target: HTMLDivElement | null) { let entered = false; for (let child = document.elementFromPoint(x, y); !entered && child; child = child.parentElement) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index baa7aa43b..0f1abb3ac 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -175,12 +175,11 @@ export class DocumentOptions { hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: boolean; - watchedDocuments?: Doc; // list of documents to "watch" in an icon doc to display a badge + watchedDocuments?: Doc; // list of documents an icon doc monitors in order to display a badge count targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) templates?: List<string>; hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images) caption?: RichTextField; - isAnnotating?: boolean; // whether we web document is annotation mode where links can't be clicked to allow annotations to be created opacity?: number; defaultBackgroundColor?: string; isLinkButton?: boolean; @@ -325,7 +324,7 @@ export namespace Docs { }], [DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, scrollHeight: 100000, _fitWidth: true } + options: { _height: 300, _fitWidth: true } }], [DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, @@ -823,7 +822,7 @@ export namespace Docs { const webProto = Prototypes.get(DocumentType.WEB); webProto.scrollHeight = 100000; // backward compatibility -- can be removed after db is reset webProto._fitWidth = true; // backward compatibility -- can be removed after db is reset - return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? "disabled" : "enabled", isAnnotating: false, _lockedTransform: true, ...options }); + return InstanceFromProto(webProto, url ? new WebField(new URL(url)) : undefined, { _chromeStatus: url ? undefined : "enabled", _lockedTransform: true, ...options }); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { @@ -1373,6 +1372,24 @@ export namespace DocUtils { } } + export function LeavePushpin(doc: Doc) { + if (doc.isPushpin) return undefined; + const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); + const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); + if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const pushpin = Docs.Create.FontIconDocument({ + title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, + icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#ACCEF7", + _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) + }); + Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); + const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); + doc._timecodeToShow = undefined; + return pushpin; + } + return undefined; + } + export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) { let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey); if (!(optionsCollection instanceof Doc)) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 23cb9634b..31c70427c 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -81,7 +81,7 @@ export class CurrentUserUtils { Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), Docs.Create.TextDocument("", { title: "text", _height: 100, system: true }) ], - { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, system: true } + { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true } ); slideTemplate.isTemplateDoc = makeTemplate(slideTemplate); doc["template-button-slides"] = CurrentUserUtils.ficon({ @@ -193,7 +193,7 @@ export class CurrentUserUtils { }; details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" ")); - const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 }; + const shared = { _autoHeight: true, _xMargin: 0 }; const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: "12px" }; const descriptionWrapperOpts = { title: "descriptions", _height: 300, _columnWidth: -1, treeViewHideTitle: true, _pivotField: "title", system: true }; @@ -228,7 +228,7 @@ export class CurrentUserUtils { title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", hidden: ComputedField.MakeFunction("IsNoviceMode()") as any, _stayInCollection: true, _hideContextMenu: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, _chromeStatus: "disabled", + _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); } else { @@ -499,7 +499,7 @@ export class CurrentUserUtils { if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, _chromeStatus: "disabled", + _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), system: true })); } else { @@ -528,7 +528,7 @@ export class CurrentUserUtils { if (doc.mySearchPanelDoc === undefined) { doc.mySearchPanelDoc = new PrefetchProxy(Docs.Create.SearchDocument({ _width: 500, _height: 300, backgroundColor: "dimGray", ignoreClick: true, _searchDoc: true, - childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, _chromeStatus: "disabled", title: "sidebar search stack", system: true + childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "sidebar search stack", system: true })) as any as Doc; } } @@ -563,7 +563,7 @@ export class CurrentUserUtils { backgroundColor: "black", ignoreClick: true, _gridGap: 0, _yMargin: 0, - _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, _chromeStatus: "disabled", system: true + _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true })); } // this resets all sidebar buttons to being deactivated @@ -596,7 +596,7 @@ export class CurrentUserUtils { // Sets up mobileMenu stacking document static setupMobileMenu() { const menu = new PrefetchProxy(Docs.Create.StackingDocument(this.setupMobileButtons(), { - _width: 980, ignoreClick: true, _lockedPosition: false, _chromeStatus: "disabled", title: "home", _yMargin: 100, system: true + _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true })); return menu; } @@ -627,26 +627,26 @@ export class CurrentUserUtils { static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { ...opts, dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, - borderRounding: "5px", boxShadow: "0 0", _chromeStatus: "disabled", system: true + borderRounding: "5px", boxShadow: "0 0", system: true }) as any as Doc // sets up the text container for the information contained within the mobile button static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { ...opts, dropAction: undefined, removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, - backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", _chromeStatus: "disabled", ignoreClick: true, system: true + backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true }) as any as Doc // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, - dropAction: undefined, title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", system: true + dropAction: undefined, title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", system: true }) as any as Doc // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, - dropAction: undefined, title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, _chromeStatus: "disabled", backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true + dropAction: undefined, title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true }) as any as Doc @@ -671,11 +671,11 @@ export class CurrentUserUtils { static setupThumbDoc(userDoc: Doc) { if (!userDoc.thumbDoc) { const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), { - _width: 100, _height: 50, ignoreClick: true, _lockedPosition: true, _chromeStatus: "disabled", title: "buttons", + _width: 100, _height: 50, ignoreClick: true, _lockedPosition: true, title: "buttons", _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white", system: true }); thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { - _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column", system: true + _width: 300, _height: 25, _autoHeight: true, linearViewIsExpanded: true, flexDirection: "column", system: true }); userDoc.thumbDoc = thumbDoc; } @@ -695,7 +695,7 @@ export class CurrentUserUtils { title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true }); return Docs.Create.StackingDocument([webDoc, uploadDoc], { - _width: screen.width, _lockedPosition: true, _chromeStatus: "disabled", title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true + _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true }); } @@ -715,7 +715,7 @@ export class CurrentUserUtils { if (doc.myCreators === undefined) { doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], { title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0, _fitWidth: true, - _width: 500, _height: 300, ignoreClick: true, _lockedPosition: true, _chromeStatus: "disabled", system: true + _width: 500, _height: 300, ignoreClick: true, _lockedPosition: true, system: true })); } // setup a color picker @@ -728,7 +728,7 @@ export class CurrentUserUtils { if (doc.myTools === undefined) { const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], { - title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _chromeStatus: "disabled", _forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, + title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, _lockedPosition: true, _forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true, })) as any as Doc; doc.myTools = toolsStack; @@ -830,7 +830,6 @@ export class CurrentUserUtils { static setupSidebarContainer(doc: Doc) { if (doc.sidebar === undefined) { const sidebarContainer = new Doc(); - sidebarContainer._chromeStatus = "disabled"; sidebarContainer.system = true; doc.sidebar = new PrefetchProxy(sidebarContainer); } @@ -852,7 +851,7 @@ export class CurrentUserUtils { static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, { ...opts, _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", _forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - backgroundColor: "black", treeViewPreventOpen: true, _lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true, system: true + backgroundColor: "black", treeViewPreventOpen: true, _lockedPosition: true, linearViewIsExpanded: true, system: true })) as any as Doc static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ @@ -922,7 +921,7 @@ export class CurrentUserUtils { if (doc.myImportDocs === undefined) { doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My ImportDocuments", _forceActive: true, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, - childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, _lockedPosition: true, _chromeStatus: "disabled", system: true + childDropAction: "alias", _autoHeight: true, _yMargin: 50, _gridGap: 15, _lockedPosition: true, system: true })); } if (doc.myImportPanel === undefined) { @@ -1200,7 +1199,7 @@ export class CurrentUserUtils { _width: width || 200, _height: height || 100, x: x, y: y, _fitWidth: true, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), _fontFamily: StrCast(Doc.UserDoc().fontFamily), title }); - const template = FormattedTextBox.DefaultLayout; + const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { tbox._width = NumCast(template._width); tbox.layoutKey = "layout_" + StrCast(template.title); diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 0d154bc3a..dc95193ea 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -157,8 +157,8 @@ export namespace DragManager { // used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made. // this is pretty clunky and should be rethought out using linkDrag or DocumentDrag export class AnchorAnnoDragData extends LinkDragData { - constructor(dragDoc: Doc, linkSourceGetAnchor: () => Doc, dropDocCreator: (annotationOn: Doc | undefined) => Doc) { - super(dragDoc, linkSourceGetAnchor); + constructor(dragView: DocumentView, linkSourceGetAnchor: () => Doc, dropDocCreator: (annotationOn: Doc | undefined) => Doc) { + super(dragView, linkSourceGetAnchor); this.dropDocCreator = dropDocCreator; this.offset = [0, 0]; } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index f8aede717..876fbac54 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -9,6 +9,7 @@ import { DateField } from '../../fields/DateField'; import { ScriptField } from '../../fields/ScriptField'; import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { DocUtils } from '../documents/Documents'; /// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView) @@ -127,16 +128,15 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T public get annotationKey() { return this.fieldKey + "-" + this._annotationKey; } @action.bound - removeDocument(doc: Doc | Doc[], annotationKey?: string): boolean { + removeDocument(doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean): boolean { const effectiveAcl = GetEffectiveAcl(this.dataDoc); const indocs = doc instanceof Doc ? [doc] : doc; const docs = indocs.filter(doc => effectiveAcl === AclEdit || effectiveAcl === AclAdmin || GetEffectiveAcl(doc) === AclAdmin); if (docs.length) { - const docs = doc instanceof Doc ? [doc] : doc; - docs.map(doc => { + setTimeout(() => docs.map(doc => { // this allows 'addDocument' to see the annotationOn field in order to create a pushin Doc.SetInPlace(doc, "isPushpin", undefined, true); - Doc.SetInPlace(doc, "annotationOn", undefined, true); - }); + doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, "annotationOn", undefined, true); + })); const targetDataDoc = this.dataDoc; const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const toRemove = value.filter(v => docs.includes(v)); @@ -144,6 +144,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T if (toRemove.length !== 0) { const recent = Cast(Doc.UserDoc().myRecentlyClosedDocs, Doc) as Doc; toRemove.forEach(doc => { + leavePushpin && DocUtils.LeavePushpin(doc); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); doc.context = undefined; recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); @@ -159,7 +160,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. @action.bound moveDocument(doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean { - return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc, annotationKey) ? addDocument(doc) : false; + return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc, annotationKey, true) ? addDocument(doc) : false; } @action.bound addDocument(doc: Doc | Doc[], annotationKey?: string): boolean { diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 5ebf29603..4a1ec4d6c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -155,7 +155,7 @@ 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, useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } @@ -250,17 +250,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV } @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 menuButton() { const targetDoc = this.view0?.props.Document; return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{`Open Context Menu`}</div></>}> @@ -377,9 +366,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (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>} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}> {this.considerGoogleDocsPush} </div>} diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 093e01a19..f5fac17a9 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -8,22 +8,22 @@ 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, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents, emptyPath } from "../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { DocUtils } from "../documents/Documents"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; -import { LinkManager } from "../util/LinkManager"; import { Scripting } from "../util/Scripting"; import { Transform } from "../util/Transform"; import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu"; import "./GestureOverlay.scss"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowEnd, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke"; import { DocumentView } from "./nodes/DocumentView"; import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; import { Touchable } from "./Touchable"; import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu"; +import { SelectionManager } from "../util/SelectionManager"; @observer export class GestureOverlay extends Touchable { @@ -827,7 +827,8 @@ export class GestureOverlay extends Touchable { } @computed get elements() { - const width = Number(ActiveInkWidth()); + const selView = SelectionManager.Views().lastElement(); + const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc.viewScale, 1) / (selView?.props.ScreenToLocalTransform().Scale || 1); const rect = this._overlayRef.current?.getBoundingClientRect(); const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true); B.left = B.left - width / 2; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 46586bf1b..34cdb50e3 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -28,19 +28,18 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } - - private analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); } - private makeMask = () => { - this.props.Document.mixBlendMode = "hard-light"; - this.props.Document.color = "#9b9b9bff"; - //this.props.Document._stayInCollection = true; - this.props.Document.isInkMask = true; - } + public static toggleMask = action((inkDoc: Doc) => { + inkDoc.isInkMask = !inkDoc.isInkMask; + inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; + inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; + inkDoc.color = "#9b9b9bff"; + inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; + }) public _prevX = 0; public _prevY = 0; @@ -207,7 +206,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const cm = ContextMenu.Instance; 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: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), 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" }); } diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 9b14c180e..731d46502 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -30,15 +30,16 @@ export class LightboxView extends React.Component<LightboxViewProps> { @observable private static _docTarget: Opt<Doc>; @observable private static _docFilters: string[] = []; // filters @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target - private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number> }>; + private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, scrollTop: Opt<number> }>; private static _history: Opt<{ doc: Doc, target?: Doc }[]> = []; private static _future: Opt<Doc[]> = []; private static _docView: Opt<DocumentView>; - static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number> }> }[] = []; + static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, scrollTop: Opt<number> }> }[] = []; @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[]) { if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { this.LightboxDoc._panX = this._savedState.panX; this.LightboxDoc._panY = this._savedState.panY; + this.LightboxDoc._scrollTop = this._savedState.scrollTop; this.LightboxDoc._viewScale = this._savedState.scale; this.LightboxDoc._viewTransition = undefined; } @@ -53,6 +54,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { panX: Cast(doc._panX, "number", null), panY: Cast(doc._panY, "number", null), scale: Cast(doc._viewScale, "number", null), + scrollTop: Cast(doc._scrollTop, "number", null), }; } } @@ -123,6 +125,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { LightboxView.LightboxDoc._panX = saved.panX; LightboxView.LightboxDoc._panY = saved.panY; LightboxView.LightboxDoc._viewScale = saved.scale; + LightboxView.LightboxDoc._scrollTop = saved.scrollTop; LightboxView.LightboxDoc._viewTransition = undefined; } const pop = LightboxView.path.pop(); diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 503c284aa..1e97f9b41 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,20 +1,22 @@ -import { action, observable, runInAction } from "mobx"; +import { action, observable, runInAction, ObservableMap } from "mobx"; import { observer } from "mobx-react"; import { Dictionary } from "typescript-collections"; import { AclAddonly, AclAdmin, AclEdit, DataSym, Doc, Opt } from "../../fields/Doc"; import { Id } from "../../fields/FieldSymbols"; +import { List } from "../../fields/List"; +import { NumCast } from "../../fields/Types"; import { GetEffectiveAcl } from "../../fields/util"; -import { DocUtils, Docs } from "../documents/Documents"; +import { Utils } from "../../Utils"; +import { Docs } from "../documents/Documents"; +import { DocumentType } from "../documents/DocumentTypes"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { DragManager } from "../util/DragManager"; +import { undoBatch } from "../util/UndoManager"; +import "./MarqueeAnnotator.scss"; +import { DocumentView } from "./nodes/DocumentView"; import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; import { AnchorMenu } from "./pdf/AnchorMenu"; -import "./MarqueeAnnotator.scss"; import React = require("react"); -import { undoBatch } from "../util/UndoManager"; -import { NumCast } from "../../fields/Types"; -import { DocumentType } from "../documents/DocumentTypes"; -import { List } from "../../fields/List"; const _global = (window /* browser */ || global /* node */) as any; export interface MarqueeAnnotatorProps { @@ -24,11 +26,12 @@ export interface MarqueeAnnotatorProps { scaling?: () => number; containerOffset?: () => number[]; mainCont: HTMLDivElement; - savedAnnotations: Dictionary<number, HTMLDivElement[]>; + docView: DocumentView; + savedAnnotations: ObservableMap<number, HTMLDivElement[]>; annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; getPageFromScroll?: (top: number) => number; - finishMarquee: () => void; + finishMarquee: (x?: number, y?: number) => void; anchorMenuClick?: (anchor: Doc) => void; } @observer @@ -46,7 +49,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { AnchorMenu.Instance.Status = "marquee"; AnchorMenu.Instance.fadeOut(true); // clear out old marquees and initialize menu for new selection - this.props.savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + Array.from(this.props.savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this.props.savedAnnotations.clear(); }); } @@ -57,8 +60,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width); this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop; this._height = this._width = 0; - document.addEventListener("pointermove", this.onSelectMove); - document.addEventListener("pointerup", this.onSelectEnd); + document.addEventListener("pointermove", this.onSelectMove, true); + document.addEventListener("pointerup", this.onSelectEnd, true); AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.(this.highlight("rgba(173, 216, 230, 0.75)", true)); AnchorMenu.Instance.Highlight = this.highlight; @@ -79,7 +82,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { FormattedTextBox.SelectOnLoad = target[Id]; return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; @@ -89,17 +92,17 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { }); } componentWillUnmount() { - document.removeEventListener("pointermove", this.onSelectMove); - document.removeEventListener("pointerup", this.onSelectEnd); + document.removeEventListener("pointermove", this.onSelectMove, true); + document.removeEventListener("pointerup", this.onSelectEnd, true); } @undoBatch @action makeAnnotationDocument = (color: string): Opt<Doc> => { - if (this.props.savedAnnotations.size() === 0) return undefined; - if ((this.props.savedAnnotations.values()[0][0] as any).marqueeing) { + if (this.props.savedAnnotations.size === 0) return undefined; + if ((Array.from(this.props.savedAnnotations.values())[0][0] as any).marqueeing) { const scale = this.props.scaling?.() || 1; - const anno = this.props.savedAnnotations.values()[0][0]; + const anno = Array.from(this.props.savedAnnotations.values())[0][0]; const containerOffset = this.props.containerOffset?.() || [0, 0]; const marqueeAnno = Docs.Create.FreeformDocument([], { backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title }); marqueeAnno.x = (parseInt(anno.style.left || "0") - containerOffset[0]) / scale; @@ -115,7 +118,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; const annoDocs: Doc[] = []; - this.props.savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => { + this.props.savedAnnotations.forEach((value: HTMLDivElement[], key: number) => value.map(anno => { const textRegion = new Doc(); textRegion.x = parseInt(anno.style.left ?? "0"); textRegion.y = parseInt(anno.style.top ?? "0"); @@ -147,20 +150,20 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { return annotationDoc as Doc ?? undefined; } - public static previewNewAnnotation = action((savedAnnotations: Dictionary<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => { + public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => { if (div.style.top) { div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); } annotationLayer.append(div); div.style.backgroundColor = "#ACCEF7"; div.style.opacity = "0.5"; - const savedPage = savedAnnotations.getValue(page); + const savedPage = savedAnnotations.get(page); if (savedPage) { savedPage.push(div); - savedAnnotations.setValue(page, savedPage); + savedAnnotations.set(page, savedPage); } else { - savedAnnotations.setValue(page, [div]); + savedAnnotations.set(page, [div]); } }); @@ -178,15 +181,20 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { } onSelectEnd = (e: PointerEvent) => { - if (!e.ctrlKey) { - AnchorMenu.Instance.Marquee = { left: this._left, top: this._top, width: this._width, height: this._height }; - } - if (this._width > 10 || this._height > 10) { // configure and show the annotation/link menu if a the drag region is big enough const marquees = this.props.mainCont.getElementsByClassName("marqueeAnnotator-dragBox"); if (marquees?.length) { // copy the temporary marquee to allow for multiple selections (not currently available though). const copy = document.createElement("div"); - ["left", "top", "width", "height", "border", "opacity"].forEach(prop => copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]); + ["border", "opacity"].forEach(prop => copy.style[prop as any] = (marquees[0] as HTMLDivElement).style[prop as any]); + const bounds = (marquees[0] as HTMLDivElement).getBoundingClientRect(); + const uitls = Utils.GetScreenTransform(marquees[0] as HTMLDivElement); + const rbounds = { top: uitls.translateY, left: uitls.translateX, width: (bounds.right - bounds.left), height: (bounds.bottom - bounds.top) }; + const otls = Utils.GetScreenTransform(this.props.annotationLayer); + const fbounds = { top: (rbounds.top - otls.translateY) / otls.scale, left: (rbounds.left - otls.translateX) / otls.scale, width: rbounds.width / otls.scale, height: rbounds.height / otls.scale }; + copy.style.top = fbounds.top.toString() + "px"; + copy.style.left = fbounds.left.toString() + "px"; + copy.style.width = fbounds.width.toString() + "px"; + copy.style.height = fbounds.height.toString() + "px"; copy.className = "marqueeAnnotator-annotationBox"; (copy as any).marqueeing = true; MarqueeAnnotator.previewNewAnnotation(this.props.savedAnnotations, this.props.annotationLayer, copy, this.props.getPageFromScroll?.(this._top) || 0); @@ -197,10 +205,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { if (AnchorMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up this.highlight("rgba(245, 230, 95, 0.75)", false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color) } + this.props.finishMarquee(); } else { runInAction(() => this._width = this._height = 0); + this.props.finishMarquee(e.clientX, e.clientY); } - this.props.finishMarquee(); } render() { diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss index ba212da7f..29d2bfcb7 100644 --- a/src/client/views/PropertiesButtons.scss +++ b/src/client/views/PropertiesButtons.scss @@ -63,6 +63,7 @@ $linkGap : 3px; display: flex; flex-direction: row; flex-wrap: wrap; + padding-bottom: 5.5px; } .onClickFlyout-editScript { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index bf72dbdba..fbcd55c47 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -1,557 +1,163 @@ -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 { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../fields/Doc"; +import { Doc, Opt } from "../../fields/Doc"; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { Cast, NumCast, StrCast } 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 { BoolCast, StrCast } from "../../fields/Types"; +import { DocUtils } from '../documents/Documents'; 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 { CollectionViewType } from './collections/CollectionView'; +import { InkingStroke } from './InkingStroke'; +import { DocumentView } from './nodes/DocumentView'; import './PropertiesButtons.scss'; import React = require("react"); -import { CollectionViewType } from './collections/CollectionView'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; - -const cloud: IconProp = "cloud-upload-alt"; -const fetch: IconProp = "sync-alt"; - enum UtilityButtonState { Default, OpenRight, OpenExternally } - @observer export class PropertiesButtons extends React.Component<{}, {}> { - private _pullAnimating = false; - private _pushAnimating = false; - private _pullColorAnimating = false; - - @observable private pushIcon: IconProp = "arrow-alt-circle-up"; - @observable private pullIcon: IconProp = "arrow-alt-circle-down"; - @observable private pullColor: string = "white"; - @observable public isAnimatingFetch = false; - @observable public isAnimatingPulse = false; - - @observable private openHover: UtilityButtonState = UtilityButtonState.Default; - @observable public static Instance: PropertiesButtons; - public static hasPushedHack = false; - public static hasPulledHack = false; - - - @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc; } - @computed get selectedDocumentView() { - if (SelectionManager.Views().length) { - return SelectionManager.Views()[0]; - } else return undefined; - } - @computed get onClick() { return this.selectedDoc?.onClickBehavior ? this.selectedDoc?.onClickBehavior : "nothing"; } + @computed get selectedDoc() { return SelectionManager.SelectedSchemaDoc() || SelectionManager.Views().lastElement()?.rootDoc; } - public startPullOutcome = action((success: boolean) => { - if (!this._pullAnimating) { - this._pullAnimating = true; - this.pullIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pullIcon = "arrow-alt-circle-down"; - this._pullAnimating = false; - }), 1000); - } - }); - - public startPushOutcome = action((success: boolean) => { - this.isAnimatingPulse = false; - if (!this._pushAnimating) { - this._pushAnimating = true; - this.pushIcon = success ? "check-circle" : "stop-circle"; - setTimeout(() => runInAction(() => { - this.pushIcon = "arrow-alt-circle-up"; - this._pushAnimating = false; - }), 1000); - } - }); - - public setPullState = action((unchanged: boolean) => { - this.isAnimatingFetch = false; - if (!this._pullColorAnimating) { - this._pullColorAnimating = true; - this.pullColor = unchanged ? "lawngreen" : "red"; - setTimeout(this.clearPullColor, 1000); - } - }); - - private clearPullColor = action(() => { - this.pullColor = "white"; - this._pullColorAnimating = false; - }); - - @computed - get considerGoogleDocsPush() { + propertyToggleBtn = (label: string, property: string, tooltip: (on?: any) => string, icon: (on: boolean) => string, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void) => { const targetDoc = this.selectedDoc; - const published = targetDoc && Doc.GetProto(targetDoc)[GoogleRef] !== undefined; - const animation = this.isAnimatingPulse ? "shadow-pulse 1s linear infinite" : "none"; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{`${published ? "Push" : "Publish"} to Google Docs`}</div>} placement="top"> - <div> - <div - className="propertiesButtons-linker" - style={{ animation }} - onClick={async () => { - await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - !published && runInAction(() => this.isAnimatingPulse = true); - PropertiesButtons.hasPushedHack = false; - targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1; - }}> - <FontAwesomeIcon className="documentdecorations-icon" icon={published ? (this.pushIcon as any) : cloud} size={published ? "lg" : "sm"} /> + const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => (dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? undefined : true; + return !targetDoc ? (null) : + <Tooltip title={<div className={`dash-tooltip`}>{tooltip(targetDoc?.[property])} </div>} placement="top"> + <div> + <div className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(":hover") ? "hover" : targetDoc[property] ? "on" : "off"}`} + onPointerDown={e => e.stopPropagation()} + onClick={undoBatch(() => { + if (SelectionManager.Views().length) { + SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); + } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); + })} > + <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} /> + </div> + <div className="propertiesButtons-title">{label}</div> </div> - <div className="propertiesButtons-title">Google</div> - </div> - </Tooltip>; + </Tooltip>; } - - @computed - get considerGoogleDocsPull() { - const targetDoc = this.selectedDoc; - const dataDoc = targetDoc && Doc.GetProto(targetDoc); - const animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none"; - - const title = (() => { - switch (this.openHover) { - default: - case UtilityButtonState.Default: return `${!dataDoc?.googleDocUnchanged ? "Pull from" : "Fetch"} Google Docs`; - case UtilityButtonState.OpenRight: return "Open in Right Split"; - case UtilityButtonState.OpenExternally: return "Open in new Browser Tab"; - } - })(); - - return !targetDoc || !dataDoc || !dataDoc[GoogleRef] ? (null) : <Tooltip - title={<><div className="dash-tooltip">{title}</div></>} placement="top"> - <div> - <div className="propertiesButtons-linker" - style={{ backgroundColor: this.pullColor }} - onPointerEnter={action(e => { - e.altKey && (this.openHover = UtilityButtonState.OpenExternally); - e.shiftKey && (this.openHover = UtilityButtonState.OpenRight); - })} - onPointerLeave={action(() => this.openHover = UtilityButtonState.Default)} - onClick={async e => { - const googleDocUrl = `https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`; - if (e.shiftKey) { - e.preventDefault(); - let googleDoc = await Cast(dataDoc.googleDoc, Doc); - if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, isAnnotating: false, useCors: false }; - googleDoc = Docs.Create.WebDocument(googleDocUrl, options); - dataDoc.googleDoc = googleDoc; - } - CollectionDockingView.AddSplit(googleDoc, "right"); - } else if (e.altKey) { - e.preventDefault(); - window.open(googleDocUrl); - } else { - this.clearPullColor(); - PropertiesButtons.hasPulledHack = false; - targetDoc[Pulls] = NumCast(targetDoc[Pulls]) + 1; - dataDoc.googleDocUnchanged && runInAction(() => this.isAnimatingFetch = true); - } - }}> - <FontAwesomeIcon className="documentdecorations-icon" size="lg" color="black" - style={{ WebkitAnimation: animation, MozAnimation: animation }} - icon={(() => { - switch (this.openHover) { - default: - case UtilityButtonState.Default: return dataDoc.googleDocUnchanged === false ? (this.pullIcon as any) : fetch; - case UtilityButtonState.OpenRight: return "arrow-alt-circle-right"; - case UtilityButtonState.OpenExternally: return "share"; - } - })()} - /> - </div> - <div className="propertiesButtons-title" style={{ backgroundColor: "white", color: "black" }}>Fetch</div> - </div> - </Tooltip>; + @computed get lockButton() { + return this.propertyToggleBtn("No\xA0Drag", "_lockedPosition", on => `${on ? "Unlock" : "Lock"} position to prevent dragging`, on => "thumbtack"); } - - @action @undoBatch - onLock = () => { - SelectionManager.Views().forEach(dv => dv.docView?.toggleLockPosition()); + @computed get dictationButton() { + return this.propertyToggleBtn("Dictate", "_showAudio", on => `${on ? "Hide" : "Show"} dictation/recording controls`, on => "microphone"); } - - @computed - get lockButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<div className="dash-tooltip">{`${this.selectedDoc?._lockedPosition ? "Unlock" : "Lock"} Position`}</div>} placement="top"> - <div> - <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" - >Position </div> - </div> - </Tooltip>; + @computed get maskButton() { + return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)); } - - @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={() => this.selectedDoc && Doc.Zip(this.selectedDoc)}> - <FontAwesomeIcon className="propertiesButtons-icon" icon="download" size="lg" /> - </div> - <div className="propertiesButtons-title"> downld </div> - </div> - </Tooltip>; + @computed get clustersButton() { + return this.propertyToggleBtn("Clusters", "_useClusters", on => `${on ? "Hide" : "Show"} clusters`, on => "braille"); } - - @undoBatch - setDictation = () => SelectionManager.Views().forEach(dv => dv.rootDoc._showAudio = !dv.rootDoc._showAudio) - - @computed - get dictationButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Dictation Controls"}</div>} placement="top"> - <div> - <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"> Dictate </div> - </div> - </Tooltip>; + @computed get panButton() { + return this.propertyToggleBtn("Lock\xA0View", "_lockedTransform", on => `${on ? "Unlock" : "Lock"} panning of view`, on => "lock"); } - - - @undoBatch - @action - setTitle = () => { - SelectionManager.Views().forEach(dv => dv.rootDoc._showTitle = !dv.rootDoc._showTitle ? "title" : dv.rootDoc._showTitle === "title" ? "title:hover" : undefined); + @computed get fitContentButton() { + return this.propertyToggleBtn("View All", "_fitToBox", on => `${on ? "Don't" : ""} fit content to container visible area`, on => "eye"); } - - @computed - get titleButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Title Header"}</div>} placement="top"> - <div> - <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._showTitle === "title" ? "on" : StrCast(targetDoc._showTitle).includes(":hover") ? "hover" : "off"}`} onPointerDown={this.setTitle}> - <FontAwesomeIcon className="propertiesButtons-icon" icon="text-width" size="lg" /> - </div> - <div className="propertiesButtons-title"> Title </div> - </div> - </Tooltip>; + @computed get fitWidthButton() { + return this.propertyToggleBtn("Fit\xA0Width", "_fitWidth", on => `${on ? "Don't" : ""} fit content to width of container`, on => "arrows-alt-h"); } - - @undoBatch - @action - setCaption = () => { - SelectionManager.Views().forEach(dv => { - dv.rootDoc._showCaption = dv.rootDoc._showCaption === undefined ? "caption" : undefined; - console.log("caption = " + dv.rootDoc._showCaption); - }); + @computed get captionButton() { + return this.propertyToggleBtn("Caption", "_showCaption", on => `${on ? "Hide" : "Show"} caption footer`, on => "closed-captioning", (dv, doc) => (dv?.rootDoc || doc)._showCaption = (dv?.rootDoc || doc)._showCaption === undefined ? "caption" : undefined); } - - @computed - get captionButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Caption Footer"}</div>} placement="top"> - <div> - <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"> Caption </div> - </div> - </Tooltip>; + @computed get chromeButton() { + return this.propertyToggleBtn("Controls", "_chromeStatus", on => `${on === "enabled" ? "Hide" : "Show"} editing UI`, on => "edit", (dv, doc) => (dv?.rootDoc || doc)._chromeStatus = (dv?.rootDoc || doc)._chromeStatus === undefined ? "enabled" : undefined); } - - @undoBatch - @action - setChrome = () => { - SelectionManager.Views().forEach(dv => dv.rootDoc._chromeStatus = dv.rootDoc._chromeStatus === "disabled" ? "enabled" : "disabled"); + @computed get titleButton() { + return this.propertyToggleBtn("Title", "_showTitle", on => "Switch between title styles", on => "text-width", (dv, doc) => (dv?.rootDoc || doc)._showTitle = !(dv?.rootDoc || doc)._showTitle ? "title" : (dv?.rootDoc || doc)._showTitle === "title" ? "title:hover" : undefined); } @computed - get chromeButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip title={<div className="dash-tooltip">{"Show Editing UI"}</div>} placement="top"> + get onClickButton() { + return !this.selectedDoc ? (null) : <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top"> <div> - <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._chromeStatus === "enabled" ? "on" : "off"}`} onPointerDown={this.setChrome}> - <FontAwesomeIcon className="propertiesButtons-icon" icon="edit" size="lg" /> + <div className="propertiesButtons-linkFlyout"> + <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}> + <div className={"propertiesButtons-linkButton-empty"} onPointerDown={e => e.stopPropagation()} > + <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" /> + </div> + </Flyout> </div> - <div className="propertiesButtons-title"> Controls </div> + <div className="propertiesButtons-title"> onclick </div> </div> </Tooltip>; } - @computed - get onClickButton() { - if (this.selectedDoc) { - return <Tooltip title={<><div className="dash-tooltip">Choose onClick behavior</div></>} placement="top"> - <div> - <div className="propertiesButtons-linkFlyout"> - <Flyout anchorPoint={anchorPoints.LEFT_TOP} - content={this.onClickFlyout}> - <div className={"propertiesButtons-linkButton-empty"} onPointerDown={e => e.stopPropagation()} > - {<FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />} - </div> - </Flyout> - </div> - <div className="propertiesButtons-title"> onclick </div> - </div> - </Tooltip>; - } else { - return null; - } - } - @undoBatch @action handleOptionChange = (e: any) => { - const value = e.target.value; this.selectedDoc && (this.selectedDoc.onClickBehavior = e.target.value); - - SelectionManager.Views().forEach(dv => { - dv.docView?.noOnClick(); - switch (value) { - case "enterPortal": dv.docView?.makeIntoPortal(); break; - case "toggleDetail": dv.docView?.toggleDetail(); break; - case "linkInPlace": dv.docView?.toggleFollowLink("inPlace", true, false); break; - case "linkOnRight": dv.docView?.toggleFollowLink("add:right", false, false); break; + SelectionManager.Views().filter(dv => dv.docView).map(dv => dv.docView!).forEach(docView => { + docView.noOnClick(); + switch (e.target.value) { + case "enterPortal": docView.makeIntoPortal(); break; + case "toggleDetail": docView.toggleDetail(); break; + case "linkInPlace": docView.toggleFollowLink("inPlace", true, false); break; + case "linkOnRight": docView.toggleFollowLink("add:right", false, false); break; } }); } - @undoBatch @action + @undoBatch editOnClickScript = () => { - if (this.selectedDoc) { - if (SelectionManager.Views().length) SelectionManager.Views().forEach(dv => DocUtils.makeCustomViewClicked(dv.rootDoc, undefined, "onClick")); - else DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); - } + if (SelectionManager.Views().length) SelectionManager.Views().forEach(dv => DocUtils.makeCustomViewClicked(dv.rootDoc, undefined, "onClick")); + else this.selectedDoc && DocUtils.makeCustomViewClicked(this.selectedDoc, undefined, "onClick"); } @computed get onClickFlyout() { - return <div><form> - <div className="radio"> - <label> - <input type="radio" value="nothing" - checked={this.onClick === 'nothing'} - onChange={this.handleOptionChange} /> - Select Document - </label> - </div> - <div className="radio"> - <label> - <input type="radio" value="enterPortal" - checked={this.onClick === 'enterPortal'} - onChange={this.handleOptionChange} /> - Enter Portal - </label> - </div> - <div className="radio"> - <label> - <input type="radio" value="toggleDetail" - checked={this.onClick === 'toggleDetail'} - onChange={this.handleOptionChange} /> - Toggle Detail - </label> - </div> - <div className="radio"> - <label> - <input type="radio" value="linkInPlace" - checked={this.onClick === 'linkInPlace'} - onChange={this.handleOptionChange} /> - Follow Link - </label> - </div> - <div className="radio"> - <label> - <input type="radio" value="linkOnRight" - checked={this.onClick === 'linkOnRight'} - onChange={this.handleOptionChange} /> - Open Link on Right - </label> - </div> - </form> + const makeLabel = (value: string, label: string) => <div className="radio"> + <label> + <input type="radio" value={value} checked={(this.selectedDoc?.onClickBehavior ?? "nothing") === value} onChange={this.handleOptionChange} /> + {label} + </label> + </div>; + return <div> + <form> + {makeLabel("nothing", "Select Document")} + {makeLabel("enterPortal", "Enter Portal")} + {makeLabel("toggleDetail", "Toggle Detail")} + {makeLabel("linkInPlace", "Follow Link")} + {makeLabel("linkOnRight", "Open Link on Right")} + </form> {Doc.UserDoc().noviceMode ? (null) : <div onPointerDown={this.editOnClickScript} className="onClickFlyout-editScript"> Edit onClick Script</div>} </div>; } - @computed - get googlePhotosButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{"Export to Google Photos"}</div></>} placement="top"> - <div> - <div className={"propertiesButtons-linkButton-empty"} - 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> - </Tooltip>; - } - - @computed - 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"> - <div> - <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" > clusters </div> - </div> - </Tooltip>; - } - - @action @undoBatch - changeFitToBox = () => { - if (this.selectedDoc) { - if (SelectionManager.Views().length) SelectionManager.Views().forEach(dv => dv.rootDoc._fitToBox = !dv.rootDoc._fitToBox); - else this.selectedDoc._fitToBox = !this.selectedDoc._fitToBox; - } - } - - @action @undoBatch - changeFitWidth = () => { - if (this.selectedDoc) { - if (SelectionManager.Views().length) SelectionManager.Views().forEach(dv => dv.rootDoc._fitWidth = !dv.rootDoc._fitWidth); - else this.selectedDoc._fitWidth = !this.selectedDoc._fitWidth; - } - } - - @action @undoBatch - changeClusters = () => { - if (this.selectedDoc) { - if (SelectionManager.Views().length) SelectionManager.Views().forEach(dv => dv.rootDoc._useClusters = !dv.rootDoc._useClusters); - else this.selectedDoc._useClusters = !this.selectedDoc._useClusters; - } - } - - @computed - get fitContentButton() { - const targetDoc = this.selectedDoc; - 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 toggle-${targetDoc._fitToBox ? "on" : "off"}`} onPointerDown={this.changeFitToBox}> - <FontAwesomeIcon className="documentdecorations-icon" icon="expand" size="lg" /> - </div> - <div className="propertiesButtons-title"> {this.selectedDoc?._fitToBox ? "unfit" : "fit"} </div> - </div> - </Tooltip>; - } - - @computed - get fitWidthButton() { - const targetDoc = this.selectedDoc; - return !targetDoc ? (null) : <Tooltip - title={<><div className="dash-tooltip">{this.selectedDoc?._fitWidth ? "Stop Fitting Width" : "Fit Width"}</div></>} placement="top"> - <div> - <div className={`propertiesButtons-linkButton-empty toggle-${targetDoc._fitWidth ? "on" : "off"}`} onPointerDown={this.changeFitWidth}> - <FontAwesomeIcon className="documentdecorations-icon" icon="arrows-alt-h" size="lg" /> - </div> - <div className="propertiesButtons-title"> {this.selectedDoc?._fitWidth ? "unfit" : "fit"} </div> - </div> - </Tooltip>; - } - - @undoBatch - @action - private makeMask = () => { - if (this.selectedDoc) { - this.selectedDoc._backgroundColor = "rgba(0,0,0,0.7)"; - this.selectedDoc.mixBlendMode = "hard-light"; - this.selectedDoc.color = "#9b9b9bff"; - this.selectedDoc._stayInCollection = true; - this.selectedDoc.isInkMask = true; - } - } - - @computed - get maskButton() { - const targetDoc = this.selectedDoc; - 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> - <div className="propertiesButtons-title"> mask </div> - </div> - </Tooltip>; - } - render() { - if (!this.selectedDoc) return (null); - - const layoutField = this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)]; + 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; - - return <div className="propertiesButtons" style={{ paddingBottom: "5.5px" }}> - <div className="propertiesButtons-button"> - {this.titleButton} - </div> - <div className="propertiesButtons-button"> - {this.captionButton} - </div> - <div className="propertiesButtons-button" style={{ display: isCollection ? "" : "none" }}> - {this.chromeButton} - </div> - <div className="propertiesButtons-button"> - {this.lockButton} - </div> - <div className="propertiesButtons-button"> - {this.dictationButton} - </div> - <div className="propertiesButtons-button"> - {this.onClickButton} - </div> - <div className="propertiesButtons-button" style={{ display: !considerPush ? "none" : "" }}> - {this.considerGoogleDocsPush} - </div> - <div className="propertiesButtons-button" style={{ display: !considerPull ? "none" : "" }}> - {this.considerGoogleDocsPull} - </div> - <div className="propertiesButtons-button" style={{ display: !isImage ? "none" : "" }}> - {this.googlePhotosButton} - </div> - - <div className="propertiesButtons-button" style={{ display: !isFreeForm ? "none" : "" }}> - {this.clustersButton} - </div> - <div className="propertiesButtons-button" style={{ display: !isFreeForm && !isText ? "none" : "" }}> - {this.fitContentButton} - </div> - <div className="propertiesButtons-button"> - {this.fitWidthButton} - </div> - <div className="propertiesButtons-button" style={{ display: !isInk ? "none" : "" }}> - {this.maskButton} - </div> - </div>; - } -} + const isCollection = this.selectedDoc?.type === DocumentType.COL; + const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform; + const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => <div className="propertiesButtons-button" style={style}> {ele} </div>; + + return !this.selectedDoc ? (null) : + <div className="propertiesButtons"> + {toggle(this.titleButton)} + {toggle(this.captionButton)} + {toggle(this.chromeButton, { display: isCollection ? "" : "none" })} + {toggle(this.lockButton)} + {toggle(this.dictationButton)} + {toggle(this.onClickButton)} + {toggle(this.clustersButton, { display: !isFreeForm ? "none" : "" })} + {toggle(this.panButton, { display: !isFreeForm ? "none" : "" })} + {toggle(this.fitContentButton, { display: !isFreeForm && !isText ? "none" : "" })} + {toggle(this.fitWidthButton)} + {toggle(this.maskButton, { display: !isInk ? "none" : "" })} + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index f39d5ee4c..9f04e8a6e 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -45,7 +45,7 @@ class OtherToggle extends React.Component<{ checked: boolean, name: string, togg export interface TemplateMenuProps { docViews: DocumentView[]; - templates?: Map<string, boolean>; + templates: Map<string, boolean>; } @@ -81,7 +81,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { @action toggleChrome = (): void => { this.props.docViews.map(dv => Doc.Layout(dv.layoutDoc)).forEach(layout => - layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : StrCast(layout._replacedChrome, "enabled"))); + layout._chromeStatus = (layout._chromeStatus ? undefined : StrCast(layout._replacedChrome, "enabled"))); } // todo: add brushes to brushMap to save with a style name @@ -115,7 +115,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { const addedTypes = Doc.UserDoc().noviceMode ? [] : 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) => + this.props.templates.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} 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={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />); @@ -140,7 +140,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { rootSelected={returnFalse} onCheckedClick={this.scriptField} onChildClick={this.scriptField} - setHeight={returnFalse} dropAction={undefined} active={returnTrue} parentActive={returnFalse} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 3556e74bc..b9757dde3 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -381,7 +381,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const otherSet = new Set<Doc>(); otherdocs?.filter(doc => !docs.includes(doc)).forEach(doc => otherSet.add(doc)); tabdocs?.filter(doc => !docs.includes(doc) && doc.type !== DocumentType.KVP).forEach(doc => otherSet.add(doc)); - const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d as Doc).filter(d => d.type !== DocumentType.KVP); + const vals = Array.from(otherSet.values()).filter(val => val instanceof Doc).map(d => d).filter(d => d.type !== DocumentType.KVP); other && (Doc.GetProto(other).data = new List<Doc>(vals)); }, 0); } diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 9b57d4c68..46bfd841e 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -187,7 +187,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { - setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e)); + setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this.props.parent.props.Document._chromeStatus && this.collapseSection(e)); this._createAliasSelected = false; } } @@ -253,7 +253,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); const style = this.props.parent; const chromeStatus = this.props.parent.props.Document._chromeStatus; - const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled'); + const showChrome = (chromeStatus !== 'view-mode' && chromeStatus); const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; return this.collapsed ? (null) : <div style={{ position: "relative" }}> @@ -286,7 +286,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr } @computed get headingView() { - const noChrome = this.props.parent.props.Document._chromeStatus === "disabled"; + const noChrome = !this.props.parent.props.Document._chromeStatus; const key = StrCast(this.props.parent.props.Document._pivotField); const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; const editableHeaderView = <EditableView diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index f054e7b7f..22b5c2b2a 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -21,7 +21,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { this.Document._pileLayoutEngine = "pass"; } this._originalChrome = StrCast(this.layoutDoc._chromeStatus); - this.layoutDoc._chromeStatus = "disabled"; + this.layoutDoc._chromeStatus = undefined; } componentWillUnmount() { this.layoutDoc._chromeStatus = this._originalChrome; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index f3c1c4464..e3d7118e9 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -63,7 +63,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } @computed get isStackingView() { return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; } @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; } - @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.pivotField && (this.chromeStatus !== 'view-mode' && this.chromeStatus)); } @computed get columnWidth() { return Math.min(this.props.PanelWidth() /* / NumCast(this.layoutDoc._viewScale, 1)*/ - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); @@ -539,7 +539,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {/* {this.chromeStatus === 'disabled' || !this.props.isSelected() ? (null) : + {/* {!this.chromeStatus || !this.props.isSelected() ? (null) : <Switch onChange={this.onToggle} onClick={this.onToggle} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 94d74b6f7..389b449b5 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -146,7 +146,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC newDoc.heading = heading; FormattedTextBox.SelectOnLoad = newDoc[Id]; FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? "" : " "; - return this.props.parent.props.addDocument?.(newDoc) || false; + return this.props.parent.addDocument?.(newDoc) || false; } @action @@ -300,7 +300,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC marginTop: NumCast(this.props.parent.yMargin, 5), width: (style.columnWidth) / ((uniqueHeadings.length + - ((this.props.parent.chromeStatus !== 'view-mode' && this.props.parent.chromeStatus !== 'disabled') ? 1 : 0)) || 1) + ((this.props.parent.chromeStatus !== 'view-mode' && this.props.parent.chromeStatus) ? 1 : 0)) || 1) }}> <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. @@ -359,7 +359,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {this.props.parent.children(this.props.docList, uniqueHeadings.length)} {singleColumn ? (null) : this.props.parent.columnDragger} </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? + {(chromeStatus !== 'view-mode' && chromeStatus && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 10 }}> <EditableView @@ -385,7 +385,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC return ( <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} style={{ - width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, + width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus) ? 1 : 0)) || 1)}%`, height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, background: this._background }} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f226cf79b..05e9ac265 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -373,16 +373,17 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } if (uriList) { console.log("Web URI = ", uriList); - const existingWebDoc = await Hypothesis.findWebDoc(uriList); - if (existingWebDoc) { - const alias = Doc.MakeAlias(existingWebDoc); - alias.x = options.x; - alias.y = options.y; - alias._nativeWidth = 850; - alias._height = 512; - alias._width = 400; - addDocument(alias); - } else { + // const existingWebDoc = await Hypothesis.findWebDoc(uriList); + // if (existingWebDoc) { + // const alias = Doc.MakeAlias(existingWebDoc); + // alias.x = options.x; + // alias.y = options.y; + // alias._nativeWidth = 850; + // alias._height = 512; + // alias._width = 400; + // addDocument(alias); + // } else + { console.log("Adding ..."); const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig) ...options, diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index be5a4b852..34fd20f1a 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -154,19 +154,7 @@ export class CollectionView extends Touchable<CollectionViewProps> { } else { added.filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))).map(doc => { // only make a pushpin if we have acl's to edit the document - const context = Cast(doc.context, Doc, null); - const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); - if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { - const pushpin = Docs.Create.FontIconDocument({ - title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, - icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#0000003d", color: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) - }); - Doc.SetInPlace(doc, "annotationOn", undefined, true); - Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); - const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); - doc._timecodeToShow = undefined; - } + DocUtils.LeavePushpin(doc); doc._stayInCollection = undefined; doc.context = this.props.Document; }); diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 53801eef1..d26f53e28 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -559,7 +559,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { 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._chromeStatus) !== "disabled" ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + {this.props.Document._chromeStatus ? <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> : undefined} {!this._showDoc ? (null) : <div className="collectionSchemaView-documentPreview" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5c5164d98..0ee2fad2e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -17,7 +17,6 @@ import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUp import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; -import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; @@ -236,7 +235,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { - if (!this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; + if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); @@ -1286,10 +1285,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @undoBatch toggleNativeDimensions = () => Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight) - @undoBatch - @action - toggleLockTransform = () => this.layoutDoc._lockedTransform = this.layoutDoc._lockedTransform ? undefined : true - onContextMenu = (e: React.MouseEvent) => { if (this.props.isAnnotationOverlay || this.props.Document.annotationOn || !ContextMenu.Instance) return; @@ -1314,7 +1309,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const optionItems = options && "subitems" in options ? options.subitems : []; !this.props.isAnnotationOverlay && !Doc.UserDoc().noviceMode && optionItems.push({ description: (this._showAnimTimeline ? "Close" : "Open") + " Animation Timeline", event: action(() => this._showAnimTimeline = !this._showAnimTimeline), icon: "eye" }); - optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" }); this.props.renderDepth && 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) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); @@ -1493,7 +1487,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.backgroundEvents ? "all" : undefined, + pointerEvents: this.backgroundEvents ? "all" : this.props.pointerEvents as any, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 9206d3767..af391a078 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -89,7 +89,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const [x, y] = this.Transform.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: 400, x, y, _height: 512, _nativeWidth: 850, isAnnotating: false, title: "bing", useCors: true }), "add:right")); + Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: "bing", useCors: true }), "add:right")); cm.displayMenu(this._downX, this._downY); e.stopPropagation(); @@ -165,7 +165,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addDocument?.(slide); e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { - FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.childLayoutString ? e.key : ""; + FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ""; FormattedTextBox.LiveTextUndo = UndoManager.StartBatch("live text batch"); this.props.addLiveTextDocument(CurrentUserUtils.GetNewTextDoc("-typed text-", x, y, 200, 100, this.props.xMargin === 0, this.props.isAnnotationOverlay ? this.props.Document : undefined)); e.stopPropagation(); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c7b2f2df6..358446a57 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -481,7 +481,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps 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 { - this.props.select(e.ctrlKey || e.shiftKey); + const ctrlPressed = e.ctrlKey || e.shiftKey; + if (this.props.Document.type === DocumentType.WEB) { + this._timeout = setTimeout(() => { this._timeout = undefined; this.props.select(ctrlPressed); }, 350); + } else this.props.select(ctrlPressed); } preventDefault = false; } @@ -593,7 +596,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); @undoBatch toggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(self, "${this.Document.layoutKey}")`); - @undoBatch toggleLockPosition = () => this.Document._lockedPosition = this.Document._lockedPosition ? undefined : true; @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { @@ -688,7 +690,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!this.Document.annotationOn) { const options = cm.findByDescription("Options..."); const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - 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" }); onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); @@ -720,7 +721,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); if (!Doc.UserDoc().noviceMode) { moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" }); + moreItems.push({ description: `${this.Document._chromeStatus ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus ? undefined : "enabled"), icon: "project-diagram" }); if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); @@ -1114,8 +1115,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { position: this.props.Document.isInkMask ? "absolute" : undefined, transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, width: xshift ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: yshift ?? this.props.Document._fitWidth ? `${this.panelHeight}px` : - `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`, + height: yshift ?? (this.props.Document._fitWidth ? `${this.panelHeight}px` : + `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} DocumentView={this.selfView} diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 56c79cde9..6ae4b9726 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -49,14 +49,12 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( 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}`} onContextMenu={this.specificContextMenu} - style={{ - backgroundColor: this.layoutDoc.iconShape === "square" ? backgroundColor : "", - }}> + style={{ backgroundColor: backgroundColor, }}> <div className="menuButton-wrap"> {icon === 'pres-trail' ? presTrailsIcon : <FontAwesomeIcon className={`menuButton-icon-${shape}`} icon={icon} color={color} size={this.layoutDoc.iconShape === "square" ? "sm" : "sm"} />} {!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)} + <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} /> </div> </button>; return !this.layoutDoc.toolTip ? button : @@ -67,7 +65,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>( } interface FontIconBadgeProps { - collection: Doc; + collection: Doc | undefined; } @observer @@ -77,7 +75,7 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> { onPointerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, (e: PointerEvent) => { - const dragData = new DragManager.DocumentDragData([this.props.collection]); + const dragData = new DragManager.DocumentDragData([this.props.collection!]); DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y); return true; }, diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index bc01acdfd..9426f6afc 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap, untracked } from 'mobx'; import { observer } from "mobx-react"; import { Dictionary } from 'typescript-collections'; import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc'; @@ -50,10 +50,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined; public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); - private _curSuffix = "_m"; private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; - @observable uploadIcon = uploadIcons.idle; + @observable _curSuffix = ""; + @observable _uploadIcon = uploadIcons.idle; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); @@ -61,17 +61,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } componentDidMount() { + this._disposers.sizer = reaction(() => ( + { + forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, + scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0], + selected: this.props.isSelected() + }), + ({ forceFull, scrSize, selected }) => this._curSuffix = forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o", + { fireImmediately: true, delay: 1000 }); this._disposers.selection = reaction(() => this.props.isSelected(), selected => !selected && setTimeout(() => { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); })); this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), - action(({ nativeSize, width }) => { + ({ nativeSize, width }) => { if (!this.layoutDoc._height) { this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth; } - }), + }, { fireImmediately: true }); } @@ -176,32 +184,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here const ext = path.extname(url.href); - const scrSize = this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight); - this._curSuffix = this.props.renderDepth < 1 || this.layoutDoc._showFullRes ? "_o" : scrSize[0] < 100 ? "_s" : scrSize[0] < 400 ? "_m" : scrSize[0] < 800 || !this.props.isSelected() ? "_l" : "_o"; return url.href.replace(ext, this._curSuffix + ext); } - @observable _smallRetryCount = 1; - @observable _mediumRetryCount = 1; - @observable _largeRetryCount = 1; - @action retryPath = () => { - if (this._curSuffix === "_s") this._smallRetryCount++; - if (this._curSuffix === "_m") this._mediumRetryCount++; - if (this._curSuffix === "_l") this._largeRetryCount++; - } - - @action onError = (error: any) => { - const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; - if (timeout < 5) { - setTimeout(this.retryPath, 500); - } else { - const original = StrCast(this.dataDoc[this.fieldKey + "-originalUrl"]); - if (error.type === "error" && original) { - this.dataDoc[this.fieldKey] = new ImageField(original); - } - } - } - considerGooglePhotosLink = () => { const remoteUrl = this.dataDoc.googlePhotosUrl; return !remoteUrl ? (null) : (<img draggable={false} @@ -231,11 +216,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD <img id={"upload-icon"} draggable={false} style={{ transformOrigin: "bottom right" }} - src={`/assets/${this.uploadIcon}`} + src={`/assets/${this._uploadIcon}`} onClick={async () => { const { dataDoc } = this; const { success, failure, idle, loading } = uploadIcons; - runInAction(() => this.uploadIcon = loading); + runInAction(() => this._uploadIcon = loading); const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] }); dataDoc[this.props.fieldKey + "-originalUrl"] = primary; let succeeded = true; @@ -245,9 +230,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD } catch { succeeded = false; } - runInAction(() => this.uploadIcon = succeeded ? success : failure); + runInAction(() => this._uploadIcon = succeeded ? success : failure); setTimeout(action(() => { - this.uploadIcon = idle; + this._uploadIcon = idle; if (data) { dataDoc[this.fieldKey] = data; } @@ -265,11 +250,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD return { nativeWidth, nativeHeight, nativeOrientation }; } - // this._curSuffix = ""; - // if (w > 20) { - // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; - // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; - // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images @@ -300,20 +280,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> <div className="imageBox-fader" > - <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + <img key="paths" ref={this._imgRef} src={srcpath} style={{ transform, transformOrigin }} draggable={false} - width={nativeWidth} - ref={this._imgRef} - onError={this.onError} /> + width={nativeWidth} /> {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> - <img className="imageBox-fadeaway" - key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys + <img className="imageBox-fadeaway" key={"fadeaway"} ref={this._imgRef} src={fadepath} style={{ transform, transformOrigin }} draggable={false} - width={nativeWidth} - ref={this._imgRef} - onError={this.onError} /></div>} + width={nativeWidth} /> + </div>} </div> {this.considerDownloadIcon} {this.considerGooglePhotosLink()} @@ -337,7 +313,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); @observable _marqueeing: number[] | undefined; - @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get annotationLayer() { return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } @@ -388,7 +364,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD </CollectionFreeFormView> {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : - <MarqueeAnnotator rootDoc={this.rootDoc} scrollTop={0} down={this._marqueeing} scaling={this.props.scaling} addDocument={this.addDocument} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + <MarqueeAnnotator rootDoc={this.rootDoc} + scrollTop={0} down={this._marqueeing} + scaling={this.props.scaling} + docView={this.props.docViewPath().lastElement()} + addDocument={this.addDocument} + finishMarquee={this.finishMarquee} + savedAnnotations={this._savedAnnotations} + annotationLayer={this._annotationLayer.current} + mainCont={this._mainCont.current} + />} </div >); } + } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index e4aa639ff..0dbe0c917 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -231,7 +231,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps, PdfDocum } else { this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])) + this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); } settingsPanel() { const pageBtns = <> diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx index 844afe1e2..682ec5356 100644 --- a/src/client/views/nodes/PresBox.tsx +++ b/src/client/views/nodes/PresBox.tsx @@ -2262,7 +2262,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; return ( - <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}> + <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeStatus ? "none" : undefined }}> {isMini ? (null) : <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }} onPointerDown={e => e.stopPropagation()} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 3e1edb927..575fbcf2e 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, ObservableMap } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; import { Dictionary } from "typescript-collections"; @@ -51,7 +51,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD private _playRegionDuration = 0; @observable static _showControls: boolean; @observable _marqueeing: number[] | undefined; - @observable _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable _screenCapture = false; @observable _clicking = false; @observable _forceCreateYouTubeIFrame = false; @@ -206,7 +206,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._disposers.selection = reaction(() => this.props.isSelected(), selected => !selected && setTimeout(() => { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); })); this._disposers.triggerVideo = reaction( @@ -345,7 +345,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD this._disposers.youtubeReactionDisposer?.(); this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek((this.layoutDoc._currentTimecode || 0))); this._disposers.youtubeReactionDisposer = reaction( - () => !this.props.Document.isAnnotating && CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true }); }; if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100); @@ -575,6 +575,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD scrollTop={0} rootDoc={this.rootDoc} down={this._marqueeing} + docView={this.props.docViewPath().lastElement()} scaling={this.marqueeFitScaling} containerOffset={this.marqueeOffset} addDocument={this.addDocWithTimecode} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 6127f82e3..f15a249da 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,10 +1,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { Dictionary } from "typescript-collections"; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from "../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { HtmlField } from "../../../fields/HtmlField"; @@ -14,29 +12,30 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { WebField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, OmitKeys, returnOne, smoothScroll, Utils, returnZero, returnTrue } from "../../../Utils"; +import { emptyFunction, getWordAtPoint, OmitKeys, returnOne, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; -import { DragManager } from "../../util/DragManager"; -import { ImageUtils } from "../../util/Import & Export/ImageUtils"; +import { DocumentType } from '../../documents/DocumentTypes'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; +import { CollectionStackingView } from "../collections/CollectionStackingView"; +import { CollectionViewType } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { DocumentDecorations } from "../DocumentDecorations"; +import { LightboxView } from "../LightboxView"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; +import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; +import { SearchBox } from "../search/SearchBox"; +import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; +import { FormattedTextBox } from "./formattedText/FormattedTextBox"; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; -import { DocumentType } from '../../documents/DocumentTypes'; import React = require("react"); -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { SearchBox } from "../search/SearchBox"; -import { CollectionStackingView } from "../collections/CollectionStackingView"; -import { StyleProp } from "../StyleProvider"; -import { FormattedTextBox } from "./formattedText/FormattedTextBox"; -import { CollectionViewType } from "../collections/CollectionView"; const htmlToText = require("html-to-text"); type WebDocument = makeInterface<[typeof documentSchema]>; @@ -46,24 +45,22 @@ const WebDocument = makeInterface(documentSchema); export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); } private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _outerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; - private _longPressSecondsHack?: NodeJS.Timeout; - private _outerRef = React.createRef<HTMLDivElement>(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); - private _iframeDragRef = React.createRef<HTMLDivElement>(); private _keyInput = React.createRef<HTMLInputElement>(); - private _ignoreScroll = ""; - private _scrollTimer: any; + @observable _scrollTimer: any; + @observable private _overlayAnnoInfo: Opt<Doc>; private _initialScroll: Opt<number>; + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @observable private _marqueeing: number[] | undefined; @observable private _url: string = "hello"; - @observable private _pressX: number = 0; - @observable private _pressY: number = 0; + @observable private _isAnnotating = false; @observable private _iframe: HTMLIFrameElement | null = null; - @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); - get scrollHeight() { return this.webpage?.scrollHeight || 1000; } - get webpage() { return this._iframe?.contentDocument?.children[0]; } + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @observable private _scrollHeight = 1500; + @computed get scrollHeight() { return this._scrollHeight; } + @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); } constructor(props: any) { super(props); @@ -71,14 +68,80 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); } - this._annotationKey = this._annotationKey + "-" + this.urlHash(this._url); + if (this.layoutDoc[this.fieldKey + "-contentWidth"] === undefined) { + this.layoutDoc[this.fieldKey + "-contentWidth"] = Doc.NativeWidth(this.layoutDoc); + } + this._annotationKey = "annotations-" + this.urlHash(this._url); + } + + @action + createTextAnnotation = (sel: Selection, selRange: Range) => { + if (this._mainCont.current) { + const clientRects = selRange.getClientRects(); + for (let i = 0; i < clientRects.length; i++) { + const rect = clientRects.item(i); + if (rect && rect.width !== this._mainCont.current.clientWidth) { + const annoBox = document.createElement("div"); + annoBox.className = "marqueeAnnotator-annotationBox"; + // transforms the positions from screen onto the pdf div + annoBox.style.top = (rect.top + this._mainCont.current.scrollTop).toString(); + annoBox.style.left = (rect.left).toString(); + annoBox.style.width = (rect.width).toString(); + annoBox.style.height = (rect.height).toString(); + this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1); + } + } + } + //this._selectionText = selRange.cloneContents().textContent || ""; + + // clear selection + if (sel.empty) { // Chrome + sel.empty(); + } else if (sel.removeAllRanges) { // Firefox + sel.removeAllRanges(); + } } + @action + iframeUp = (e: PointerEvent) => { + if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) { + this._iframe.contentDocument.addEventListener("pointerup", this.iframeUp); + const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const sel = this._iframe.contentWindow.getSelection(); + if (sel) { + this.createTextAnnotation(sel, sel.getRangeAt(0)); + AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, + e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); + } + } else AnchorMenu.Instance.fadeOut(true); + } + @action + iframeDown = (e: PointerEvent) => { + const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!); + const scale = (this.props.scaling?.() || 1) * mainContBounds.scale; + const word = getWordAtPoint(e.target, e.clientX, e.clientY); + this._marqueeing = [e.clientX * scale + mainContBounds.translateX, + e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]; + if (word) { + this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp); + setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. + } else { + this._isAnnotating = true; + this.props.select(false); + e.stopPropagation(); + e.preventDefault(); + } + } + + @action iframeLoaded = (e: any) => { const iframe = this._iframe; if (iframe?.contentDocument) { - if (this._initialScroll !== undefined && this._outerRef.current && this.webpage) { - this.webpage.scrollTop = this._initialScroll; + iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); + this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight); + setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000); + if (this._initialScroll !== undefined && this._outerRef.current) { this._outerRef.current.scrollTop = this._initialScroll; this._initialScroll = undefined; } @@ -90,76 +153,57 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } if (href) { this.submitURL(href.replace(Utils.prepend(""), Cast(this.dataDoc[this.fieldKey], WebField, null)?.url.origin)); - if (this.webpage) { - this.webpage.scrollTop = NumCast(this.layoutDoc._scrollTop); - this.webpage.scrollLeft = 0; + if (this._outerRef.current) { + this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); + this._outerRef.current.scrollLeft = 0; } } }))); iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); - iframe.contentDocument.addEventListener('scroll', this.iframeScroll, false); + //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); + iframe.contentDocument.addEventListener('scroll', () => { + console.log("Scroll = " + this._iframe?.scrollTop) + } + , true); } } - resetIgnoreScroll = () => { + @action + setDashScrollTop = (scrollTop: number, timeout: number = 250) => { + const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); + timeout = scrollTop > iframeHeight ? 0 : timeout; this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout(() => { + this._scrollTimer = setTimeout(action(() => { this._scrollTimer = undefined; - this._ignoreScroll = ""; - }, 250); - this._outerRef.current && (this._outerRef.current.scrollLeft = 0); + if (!LinkDocPreview.LinkInfo && this._outerRef.current && + (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + } + }), timeout); } + @action iframeWheel = (e: any) => { - this._ignoreScroll = "iframe"; - this.resetIgnoreScroll(); - e.stopPropagation(); - } - onWebWheel = (e: React.WheelEvent) => { - this._ignoreScroll = "iframe"; - this.goTo(Math.max(0, (this.webpage?.scrollTop || 0) + (this._accumulatedGoTo + 1) * e.deltaY), 100); - this.resetIgnoreScroll(); - e.stopPropagation(); - } - onWheel = (e: React.WheelEvent) => { - this._ignoreScroll = "outer"; - this.resetIgnoreScroll(); - e.stopPropagation(); - } - iframeScroll = (e: any) => { - if (!this._ignoreScroll.includes("outer") && this._outerRef.current) { - this._outerRef.current.scrollTop = this.webpage?.scrollTop || 0; - this.layoutDoc._scrollTop = this.webpage?.scrollTop; + if (!this._scrollTimer) { + this._scrollTimer = setTimeout(action(() => this._scrollTimer = undefined), 250); // this turns events off on the iframe which allows scrolling to change direction smoothly } } - onScroll = (e: any) => { - if (!this._ignoreScroll.includes("iframe") && this.webpage) { - this.webpage.scrollTop = this._outerRef.current?.scrollTop || 0; - this.layoutDoc._scrollTop = this._outerRef.current?.scrollTop; - } + onWheel = (e: any) => { + e.stopPropagation(); + e.preventDefault(); } + onScroll = (e: any) => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0); scrollFocus = (doc: Doc, smooth: boolean) => { - let focusSpeed: Opt<number>; - if (doc !== this.rootDoc && this.webpage && this._outerRef.current) { + if (doc !== this.rootDoc && this._outerRef.current) { const scrollTo = doc.type === DocumentType.TEXTANCHOR ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), this.props.PanelHeight() / (this.props.scaling?.() || 1)); if (scrollTo !== undefined) { + const focusSpeed = smooth ? 500 : 0; this._initialScroll !== undefined && (this._initialScroll = scrollTo); - if (!LinkDocPreview.LinkInfo) { - this._ignoreScroll = "iframe|outer"; - this.layoutDoc._scrollTop = scrollTo; - this._ignoreScroll = ""; - } - this._ignoreScroll = "iframe|outer"; - this.goTo(scrollTo, focusSpeed = smooth ? 500 : 0); - setTimeout(() => { - this._scrollTimer = undefined; - this._ignoreScroll = ""; - }, focusSpeed); + this.goTo(scrollTo, focusSpeed); + return focusSpeed; } - } else { - this._initialScroll = NumCast(doc.y); } - - return focusSpeed; + this._initialScroll = NumCast(doc.y); + return 0; } getAnchor = () => { @@ -182,12 +226,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this._disposers.selection = reaction(() => this.props.isSelected(), selected => !selected && setTimeout(() => { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); })); - document.addEventListener("pointerup", this.onLongPressUp); - document.addEventListener("pointermove", this.onLongPressMove); const field = Cast(this.rootDoc[this.props.fieldKey], WebField); if (field?.url.href.indexOf("youtube") !== -1) { const youtubeaspect = 400 / 315; @@ -226,29 +268,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum quickScroll = false; } - _accumulatedGoTo = 0; - _resetGoTo: { resetGoTo: { to: number, duration: number } | undefined } = { resetGoTo: undefined }; goTo = (scrollTop: number, duration: number) => { - if (this._outerRef.current && this.webpage) { + if (this._outerRef.current) { + const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight()); + scrollTop = scrollTop > iframeHeight + 50 ? iframeHeight : scrollTop; if (duration) { - if (this._accumulatedGoTo++) { - this._resetGoTo.resetGoTo = { to: scrollTop, duration }; - } else { - smoothScroll(duration, [this.webpage as any as HTMLElement, this._outerRef.current], scrollTop, () => this._accumulatedGoTo = 0, this._resetGoTo); - } + smoothScroll(duration, [this._outerRef.current], scrollTop); + this.setDashScrollTop(scrollTop, duration); } else { - this.webpage.scrollTop = scrollTop; - this._outerRef.current.scrollTop = scrollTop; + this.setDashScrollTop(scrollTop); } } } componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener("pointerup", this.onLongPressUp); - document.removeEventListener("pointermove", this.onLongPressMove); - this._iframe?.removeEventListener('wheel', this.iframeWheel); - this._iframe?.removeEventListener('scroll', this.iframeScroll); + this._iframe?.removeEventListener('wheel', this.iframeWheel, true); } @action @@ -258,7 +293,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (future.length) { history.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = future.pop()!)); - this._annotationKey = this.fieldKey + "-annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + this.urlHash(this._url); return true; } return false; @@ -272,7 +307,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else future.push(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(this._url = history.pop()!)); - this._annotationKey = this.fieldKey + "-annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + this.urlHash(this._url); return true; } return false; @@ -299,7 +334,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum future && (future.length = 0); } this._url = newUrl; - this._annotationKey = this.fieldKey + "-annotations-" + this.urlHash(this._url); + this._annotationKey = "annotations-" + this.urlHash(this._url); this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); } catch (e) { console.log("WebBox URL error:" + this._url); @@ -353,114 +388,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum ); } - 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>; - } - - _ignore = 0; - onPreWheel = (e: React.WheelEvent) => this._ignore = e.timeStamp; - onPrePointer = (e: React.PointerEvent) => this._ignore = e.timeStamp; - onPostPointer = (e: React.PointerEvent) => this._ignore !== e.timeStamp && e.stopPropagation(); - onPostWheel = (e: React.WheelEvent) => this._ignore !== e.timeStamp && e.stopPropagation(); - - onLongPressDown = (e: React.PointerEvent) => { - this._pressX = e.clientX; - this._pressY = e.clientY; - - // find the pressed element in the iframe (currently only works if its an img) - let pressedElement: HTMLElement | undefined; - let pressedBound: ClientRect | undefined; - let selectedText: string = ""; - let pressedImg: boolean = false; - if (this._iframe) { - const B = this._iframe.getBoundingClientRect(); - const iframeDoc = this._iframe.contentDocument; - if (B && iframeDoc) { - // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload - const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top); - if (element && element.nodeName === "IMG") { - pressedBound = element.getBoundingClientRect(); - pressedElement = element.cloneNode(true) as HTMLElement; - pressedImg = true; - } else { - // check if there is selected text - const text = iframeDoc.getSelection(); - if (text && text.toString().length > 0) { - selectedText = text.toString(); - - // get html of the selected text - const range = text.getRangeAt(0); - const contents = range.cloneContents(); - const div = document.createElement("div"); - div.appendChild(contents); - pressedElement = div; - - pressedBound = range.getBoundingClientRect(); - } - } - } - } - - // mark the pressed element - if (pressedElement && pressedBound) { - if (this._iframeIndicatorRef.current) { - this._iframeIndicatorRef.current.style.top = pressedBound.top + "px"; - this._iframeIndicatorRef.current.style.left = pressedBound.left + "px"; - this._iframeIndicatorRef.current.style.width = pressedBound.width + "px"; - this._iframeIndicatorRef.current.style.height = pressedBound.height + "px"; - this._iframeIndicatorRef.current.classList.add("active"); - } - } - - // start dragging the pressed element if long pressed - this._longPressSecondsHack = setTimeout(() => { - if (pressedImg && pressedElement && pressedBound) { - e.stopPropagation(); - e.preventDefault(); - if (pressedElement.nodeName === "IMG") { - const src = pressedElement.getAttribute("src"); // TODO: may not always work - if (src) { - const doc = Docs.Create.ImageDocument(src); - ImageUtils.ExtractExif(doc); - - // add clone to div so that dragging ghost is placed properly - if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); - - const dragData = new DragManager.DocumentDragData([doc]); - DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true }); - } - } - } else if (selectedText && pressedBound && pressedElement) { - e.stopPropagation(); - e.preventDefault(); - // create doc with the selected text's html - const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML); - - // create dragging ghost with the selected text - if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement); - - // start the drag - const dragData = new DragManager.DocumentDragData([doc]); - DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true }); - } - }, 1500); - } - onLongPressMove = (e: PointerEvent) => { - // this._pressX = e.clientX; - // this._pressY = e.clientY; - } - onLongPressUp = (e: PointerEvent) => { - this._longPressSecondsHack && clearTimeout(this._longPressSecondsHack); - this._iframeIndicatorRef.current?.classList.remove("active"); - while (this._iframeDragRef.current?.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild); - } - specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; @@ -479,12 +406,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } else if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(field.url.href) : field.url.href; // view = <iframe className="webBox-iframe" src={url} onLoad={e => { e.currentTarget.before((e.currentTarget.contentDocument?.body || e.currentTarget.contentDocument)?.children[0]!); e.currentTarget.remove(); }} - view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded} + view = <iframe className="webBox-iframe" enable-annotation={"true"} + style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} + ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} 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"} />; sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin"} />; } else { - view = <iframe className="webBox-iframe" enable-annotation={"true"} ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; + view = <iframe className="webBox-iframe" enable-annotation={"true"} + style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly + ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={"https://crossorigin.me/https://cs.brown.edu"} />; } return view; } @@ -509,7 +440,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum } else { this.layoutDoc.nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]); } - this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])) + this.layoutDoc._width = NumCast(this.layoutDoc._nativeWidth) * (NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) / NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"])); } sidebarKey = () => this.fieldKey + "-sidebar"; sidebarFiltersHeight = () => 50; @@ -576,36 +507,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum @computed get content() { - const frozen = !this.props.isSelected() || DocumentDecorations.Instance?.Interacting; - const scale = this.props.scaling?.() || 1; - return (<> - <div className={"webBox-cont" + (this.props.isSelected() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} - style={{ - width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / scale}%`, - height: `${100 / scale}%`, - transform: `scale(${scale})` - }} - onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> - {this.urlContent} - </div> - {!frozen ? (null) : - <div className="webBox-overlay" style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? 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>} - </>); + return <div className={"webBox-cont" + (this.active() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} + style={{ width: NumCast(this.layoutDoc[this.fieldKey + "-contentWidth"]) || `${100 / (this.props.scaling?.() || 1)}%`, }}> + {this.urlContent} + </div>; } - @computed get allAnnotations() { return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]); } - @computed get nonDocAnnotations() { return this.allAnnotations.filter(a => a.annotations); } + showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } @computed get annotationLayer() { TraceMobx(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => - <Annotation {...this.props} showInfo={emptyFunction} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />) + {this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map(anno => + <Annotation {...this.props} fieldKey={this.annotationKey} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />) } </div>; } @@ -617,9 +531,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum this.props.select(false); } } - - @action - finishMarquee = () => this._marqueeing = undefined; + setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func; + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false); + } panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0); panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document); @@ -628,50 +545,54 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum const inactiveLayer = this.props.layerProvider?.(this.layoutDoc) === false; const scale = this.props.scaling?.() || 1; return ( - <div className="webBox" ref={this._mainCont} > + <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.active() || SnappingManager.GetIsDragging() ? undefined : "none" }} > <div className={`webBox-container`} style={{ pointerEvents: inactiveLayer ? "none" : undefined }} - onWheel={this.onWebWheel} onContextMenu={this.specificContextMenu}> <base target="_blank" /> - {this.content} <div className={"webBox-outerContent"} ref={this._outerRef} style={{ - width: `calc(${100 / scale}% - ${this.sidebarWidth()}px)`, height: `${100 / scale}%`, transform: `scale(${scale})`, - pointerEvents: !this.layoutDoc.isAnnotating || inactiveLayer ? "none" : "all" + width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale}px)`, + height: `${100 / scale}%`, + transform: `scale(${scale})`, + pointerEvents: inactiveLayer ? "none" : undefined }} onWheel={this.onWheel} - onPointerDown={this.onMarqueeDown} onScroll={this.onScroll} + onPointerDown={this.onMarqueeDown} > <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents: inactiveLayer ? "none" : undefined }}> + {this.content} <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} CollectionView={undefined} fieldKey={this.annotationKey} isAnnotationOverlay={true} scaling={returnOne} + pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} ScreenToLocalTransform={this.scrollXf} + setPreviewCursor={this.setPreviewCursor} removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} select={emptyFunction} active={this.active} whenActiveChanged={this.whenActiveChanged} /> + {this.annotationLayer} </div> </div> - {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator rootDoc={this.rootDoc} anchorMenuClick={this.anchorMenuClick} - scrollTop={NumCast(this.rootDoc._scrollTop)} - down={this._marqueeing} scaling={this.props.scaling} + scrollTop={0} + down={this._marqueeing} scaling={returnOne} addDocument={this.addDocument} + docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} @@ -682,7 +603,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum <FontAwesomeIcon style={{ color: "white" }} icon={"chevron-left"} size="sm" /> </button> {this.sidebarOverlay} - {this.props.isSelected() ? this.editToggleBtn() : null} </div>); } }
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9259e6c25..e4c481014 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -84,9 +84,6 @@ type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, da export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); - public static get DefaultLayout() { - return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null); - } public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; static _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; @@ -229,7 +226,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.rootDoc, this.getAnchor, targetCreator), e.pageX, e.pageY); + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), this.getAnchor, targetCreator), e.pageX, e.pageY); }); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); @@ -1203,19 +1200,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp prosediv && (prosediv.keeplocation = undefined); const pos = this._editorView?.state.selection.$from.pos || 1; keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); - const coords = !Number.isNaN(this._downX) ? { left: this._downX, top: this._downY, bottom: this._downY, right: this._downX } : this._editorView?.coordsAtPos(pos); - - // jump rich text menu to this textbox - const bounds = this._ref.current?.getBoundingClientRect(); - 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) { - y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height); - } - this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); - setTimeout(() => window.document.activeElement === this.ProseRef?.children[0] && RichTextMenu.Instance.jumpTo(x, y), 250); - } + + this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is selected and scrollable, stop event to prevent, say, outer collection from zooming. @@ -1421,11 +1407,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp </div>; } @computed get sidebarHandle() { + TraceMobx(); const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; return (!annotated && !this.active()) ? (null) : <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, - background: this.props.styleProvider?.(this.props.Document, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : "")) + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : "")) }} />; } @computed get sidebarCollection() { diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 0a1e0ba8f..1e2d72254 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -51,7 +51,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public PinToPres: () => void = unimplementedFunction; public MakePushpin: () => void = unimplementedFunction; public IsPushpin: () => boolean = returnFalse; - public Marquee: { left: number; top: number; width: number; height: number; } | undefined; public get Active() { return this._left > 0; } constructor(props: Readonly<{}>) { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index c9bee5101..91bb321b2 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx"; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; @@ -65,7 +65,7 @@ interface IViewerProps extends FieldViewProps { export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) { static _annotationStyle: any = addStyleSheet(); @observable private _pageSizes: { width: number, height: number }[] = []; - @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @observable private _script: CompiledScript = CompileScript("return true") as CompiledScript; @observable private _marqueeing: number[] | undefined; @observable private _textSelecting = true; @@ -140,8 +140,8 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._disposers.selected = reaction(() => this.props.isSelected(), selected => { if (!selected) { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); + Array.from(this._savedAnnotations.keys()).forEach(k => this._savedAnnotations.set(k, [])); } (SelectionManager.Views().length === 1) && this.setupPdfJsViewer(); }, @@ -390,7 +390,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. // clear out old marquees and initialize menu for new selection AnchorMenu.Instance.Status = "marquee"; - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + Array.from(this._savedAnnotations.values()).forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.clear(); this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" }); document.addEventListener("pointerup", this.onSelectEnd); @@ -400,7 +400,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu } @action - finishMarquee = () => { + finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; this._textSelecting = true; document.removeEventListener("pointermove", this.onSelectMove); @@ -574,6 +574,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu anchorMenuClick={this.props.anchorMenuClick} addDocument={this.addDocument} finishMarquee={this.finishMarquee} + docView={this.props.docViewPath().lastElement()} getPageFromScroll={this.getPageFromScroll} savedAnnotations={this._savedAnnotations} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} |