diff options
Diffstat (limited to 'src/client/views/nodes')
31 files changed, 986 insertions, 434 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 9cc4b1f9a..fe34d6687 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -7,7 +7,7 @@ import { listSpec } from "../../../fields/Schema"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { numberRange } from "../../../Utils"; +import { DashColor, numberRange } from "../../../Utils"; import { DocumentManager } from "../../util/DocumentManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; @@ -18,7 +18,6 @@ import { StyleProp } from "../StyleProvider"; import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; import React = require("react"); -import Color = require("color"); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined; @@ -165,7 +164,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelHeight: this.panelHeight, }; const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (background && Color(background).alpha() !== 1 ? "multiply" : undefined); + const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined); return <div className={"collectionFreeFormDocumentView-container"} style={{ outline: this.Highlight ? "orange solid 2px" : "", diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 44cf5d046..660045a6f 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -6,6 +6,7 @@ position: relative; z-index: 0; pointer-events: none; + display: flex; .clip-div { position: absolute; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index 6708a08ee..750213e67 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -15,6 +15,7 @@ import "./ComparisonBox.scss"; import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import React = require("react"); +import { DocumentType } from '../../documents/DocumentTypes'; export const comparisonSchema = createSchema({}); @@ -49,7 +50,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl } private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { + e.button !== 2 && setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => { // on click, animate slider movement to the targetWidth this._animating = "all 200ms"; this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth(); @@ -87,14 +88,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div>; }; const displayDoc = (which: string) => { - const whichDoc = Cast(this.dataDoc[`compareBox-${which}`], Doc, null); + const whichDoc = Cast(this.dataDoc[which], Doc, null); + //if (whichDoc?.type === DocumentType.MARKER) + const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc; return whichDoc ? <> - <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + <DocumentView + ref={(r) => { + whichDoc !== targetDoc && r?.focus(targetDoc); + }} + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} isContentActive={returnFalse} isDocumentActive={returnFalse} styleProvider={this.docStyleProvider} - Document={whichDoc} + Document={targetDoc} DataDoc={undefined} + hideLinkButton={true} pointerEvents={"none"} /> {clearButton(which)} </> : // placeholder image if doc is missing @@ -105,16 +113,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const displayBox = (which: string, index: number, cover: number) => { return <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} - ref={ele => this.createDropTarget(ele, `compareBox-${which}`, index)} > + ref={ele => this.createDropTarget(ele, which, index)} > {displayDoc(which)} </div>; }; return ( <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox("after", 1, this.props.PanelWidth() - 3)} + {displayBox(this.fieldKey === "data" ? "compareBox-after" : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)} <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}> - {displayBox("before", 0, 0)} + {displayBox(this.fieldKey === "data" ? "compareBox-before" : `${this.fieldKey}1`, 0, 0)} </div> <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)`, cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? "e-resize" : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? "w-resize" : undefined }} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 7f164ca48..1ec7bf72a 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -175,6 +175,7 @@ position: absolute; bottom: 0; width: 100%; + overflow-y: scroll; transform-origin: bottom left; opacity: 0.1; transition: opacity 0.5s; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index e8a78d75c..949e0e168 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -13,7 +13,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Ty import { AudioField } from "../../../fields/URLField"; import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util'; import { MobileInterface } from '../../../mobile/MobileInterface'; -import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils, lightOrDark, simulateMouseClick } from "../../../Utils"; +import { emptyFunction, hasDescendantTarget, OmitKeys, returnTrue, returnVal, Utils, lightOrDark, simulateMouseClick, returnEmptyString } from "../../../Utils"; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from '../../documents/DocumentTypes'; @@ -25,7 +25,6 @@ import { InteractionUtils } from '../../util/InteractionUtils'; import { LinkManager } from '../../util/LinkManager'; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from "../../util/SelectionManager"; -import { ColorScheme } from "../../util/SettingsManager"; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from "../../util/Transform"; @@ -50,6 +49,7 @@ import { ScriptingBox } from "./ScriptingBox"; import { PresBox } from './trails/PresBox'; import React = require("react"); import { IconProp } from "@fortawesome/fontawesome-svg-core"; +import { ColorScheme } from "../../util/SettingsManager"; const { Howl } = require('howler'); interface Window { @@ -90,12 +90,14 @@ export interface DocComponentView { getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) playFrom?: (time: number, endTime?: number) => void; + Pause?: () => void; setFocus?: () => void; - componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element; + componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; fieldKey?: string; annotationKey?: string; getTitle?: () => string; getScrollHeight?: () => number; + search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; } export interface DocumentViewSharedProps { renderDepth: number; @@ -486,8 +488,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.onDoubleClickHandler) { this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); } else clickFunc(); - } else if (this.Document["onClick-rawScript"] && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) {// bcz: hack? don't edit a script if you're clicking on a scripting box itself - this.props.addDocTab(DocUtils.makeCustomViewClicked(Doc.MakeAlias(this.props.Document), undefined, "onClick"), "add:right"); } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); } else { @@ -555,10 +555,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } } - onPointerUp = (e: PointerEvent): void => { + cleanupPointerEvents = () => { this.cleanUpInteractions(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); + } + + onPointerUp = (e: PointerEvent): void => { + this.cleanupPointerEvents(); if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); @@ -577,8 +581,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.Document._isLinkButton && !this.onClickHandler) { this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; - } else { - this.Document.onClick = this.layoutDoc.onClick = undefined; + } else if (this.Document._isLinkButton && this.onClickHandler) { + this.Document._isLinkButton = false; + this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; } } @undoBatch @action @@ -797,12 +802,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); } + collectionFilters = () => StrListCast(this.props.Document._docFilters); + collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); + @computed get showFilterIcon() { + return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : + this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; + } rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; panelHeight = () => this.props.PanelHeight() - this.headerMargin; screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); contentScaling = () => this.ContentScale; onClickFunc = () => this.onClickHandler; - setHeight = (height: number) => this.layoutDoc._height = height; + setHeight = (height: number) => { + if (this.props.renderDepth !== -1) { + this.layoutDoc._height = height; + } + } setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); isContentActive = (outsideReaction?: boolean) => { return CurrentUserUtils.SelectedTool !== InkTool.None || @@ -827,7 +842,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div>; return <div className="documentView-contentsView" style={{ - pointerEvents: this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none", + pointerEvents: this.props.pointerEvents as any ? this.props.pointerEvents as any : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"), height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView key={1} {...this.props} @@ -843,7 +858,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps focus={this.focus} layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} - {this.hideLinkButton ? (null) : + {this.hideLinkButton || this.props.renderDepth === -1 ? (null) : <div style={{ transformOrigin: "top left", transform: `scale(${Math.min(1, this.props.ScreenToLocalTransform().scale(this.props.ContentScaling?.() || 1).Scale)})` }}> <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} /> </div>} @@ -864,6 +879,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps anchorPanelHeight = () => this.props.PanelHeight() || 1; anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { switch (property) { + case StyleProp.ShowTitle: return ""; case StyleProp.PointerEvents: return "none"; case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox default: return this.props.styleProvider?.(doc, props, property); @@ -896,6 +912,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} + showTitle={returnEmptyString} + hideCaptions={true} fitWidth={returnTrue} styleProvider={this.anchorStyleProvider} removeDocument={this.hideLinkAnchor} @@ -958,19 +976,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); @computed get innards() { TraceMobx(); + const ffscale = (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1); const showTitle = this.ShowTitle?.split(":")[0]; const showTitleHover = this.ShowTitle?.includes(":hover"); const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; const captionView = !showCaption ? (null) : <div className="documentView-captionWrapper" - style={{ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, }}> + style={{ + pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined, + minWidth: 50 * ffscale, + maxHeight: `max(100%, ${20 * ffscale}px)` + }}> <FormattedTextBox {...OmitKeys(this.props, ['children']).omit} yPadding={10} xPadding={10} fieldKey={showCaption} - fontSize={Math.min(32, 12 * this.props.ScreenToLocalTransform().Scale)} + fontSize={12 * Math.max(1, 2 * ffscale / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} + noSidebar={true} + dontScale={true} isContentActive={this.isContentActive} onClick={this.onClickFunc} /> @@ -1040,9 +1065,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps render() { TraceMobx(); const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = (Doc.UserDoc().colorScheme === ColorScheme.Dark ? - ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] : - ["transparent", "#4476F7", "#4476F7", "yellow", "magenta", "cyan", "orange"])[highlightIndex]; + const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex]; const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex]; const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; @@ -1082,6 +1105,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> </> } + {this.showFilterIcon ? + <FontAwesomeIcon icon={"filter"} size="lg" + style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }} + onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })} + /> + : (null)} </div>; } } @@ -1122,10 +1151,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.fitWidth || this.props.PanelHeight() / this.effectiveNativeHeight > this.props.PanelWidth() / this.effectiveNativeWidth) { - return Math.max(minTextScale, this.props.PanelWidth() / this.effectiveNativeWidth); // width-limited or fitWidth + if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth } - return Math.max(minTextScale, this.props.PanelHeight() / this.effectiveNativeHeight); // height-limited or unscaled + return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } @@ -1148,7 +1177,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { } const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this.docView.props.LayoutTemplateString?.includes("LinkAnchorBox")) { + if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); if (docuBox.length) return docuBox[0].getBoundingClientRect(); } diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss index e69de29bb..6c9d53d10 100644 --- a/src/client/views/nodes/EquationBox.scss +++ b/src/client/views/nodes/EquationBox.scss @@ -0,0 +1,3 @@ +.equationBox-cont { + transform-origin: top left; +}
\ No newline at end of file diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index dacafcdf4..f1f802c13 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -84,10 +84,16 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps, EquationDo } render() { TraceMobx(); - return (<div onPointerDown={e => !e.ctrlKey && e.stopPropagation()} + const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + return (<div className="equationBox-cont" + onPointerDown={e => !e.ctrlKey && e.stopPropagation()} style={{ + transform: `scale(${scale})`, + width: `${100 / scale}%`, + height: `${100 / scale}%`, pointerEvents: !this.props.isSelected() ? "none" : undefined, }} + onKeyDown={e => e.stopPropagation()} > <EquationEditor ref={this._ref} value={this.dataDoc.text || "x"} diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index e9f19bf9e..2041c7399 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -377,9 +377,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc </div> <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={this.changeBool}> - <option value="AND" key="AND" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "AND"}>AND</option> - <option value="OR" key="OR" selected={(FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean === "OR"}>OR</option> + <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean)}> + <option value="AND" key="AND">AND</option> + <option value="OR" key="OR">OR</option> </select> <div className="filterBox-select-text">filters together</div> </div> diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index b00f97236..5050fc2d2 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -1,18 +1,16 @@ -import EquationEditor from 'equation-editor-react'; import functionPlot from "function-plot"; +import { action, computed, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; -import { createSchema, makeInterface, listSpec } from '../../../fields/Schema'; -import { StrCast, Cast } from '../../../fields/Types'; +import { List } from '../../../fields/List'; +import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; +import { Cast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; +import { Docs } from '../../documents/Documents'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; -import './LabelBox.scss'; -import { DocListCast, Doc } from '../../../fields/Doc'; -import { computed, action, reaction } from 'mobx'; -import { Docs } from '../../documents/Documents'; -import { List } from '../../../fields/List'; const EquationSchema = createSchema({}); diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 2536dbe16..4238f6d29 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -13,6 +13,7 @@ width: 100%; pointer-events: none; mix-blend-mode: multiply; // bcz: makes text fuzzy! + overflow: hidden; } .imageBox-fader { @@ -101,7 +102,6 @@ margin: 0 auto; display: flex; height: 100%; - overflow: auto; .imageBox-fadeBlocker { width: 100%; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b41bfd3ea..89f70985c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -81,11 +81,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }), ({ 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(() => { - // 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]() }), ({ nativeSize, width }) => { if (!this.layoutDoc._height) { @@ -291,7 +286,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } return <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> - <div className="imageBox-fader" > + <div className="imageBox-fader" style={{ overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined }} > <img key="paths" ref={this._imgRef} src={srcpath} style={{ transform, transformOrigin }} @@ -359,15 +354,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }} > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} - isAnnotationOverlay={true} fieldKey={this.annotationKey} CollectionView={undefined} + isAnnotationOverlay={true} annotationLayerHostsContent={true} PanelWidth={this.props.PanelWidth} PanelHeight={this.props.PanelHeight} ScreenToLocalTransform={this.screenToLocalTransform} - scaling={returnOne} select={emptyFunction} + scaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js new file mode 100644 index 000000000..db43551d8 --- /dev/null +++ b/src/client/views/nodes/LabelBigText.js @@ -0,0 +1,241 @@ +/* +Brorlandi/big-text.js v1.0.0, 2017 +Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014 +And from Jetroid/bigtext.js v1.0.0, September 2016 + +Usage: +BigText("#myElement",{ + rotateText: {Number}, (null) + fontSizeFactor: {Number}, (0.8) + maximumFontSize: {Number}, (null) + limitingDimension: {String}, ("both") + horizontalAlign: {String}, ("center") + verticalAlign: {String}, ("center") + textAlign: {String}, ("center") + whiteSpace: {String}, ("nowrap") +}); + + +Original Projects: +https://github.com/DanielHoffmann/jquery-bigtext +https://github.com/Jetroid/bigtext.js + +Options: + +rotateText: Rotates the text inside the element by X degrees. + +fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8 + +maximumFontSize: maximum font size to use. + +limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height. + +horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center". + +verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center". + +textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text. + +whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.) + +Bruno Orlandi - 2017 + +Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies +Copyright (C) 2016 Jet Holt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +function _calculateInnerDimensions(computedStyle) { + //Calculate the inner width and height + var innerWidth; + var innerHeight; + + var width = parseInt(computedStyle.getPropertyValue("width")); + var height = parseInt(computedStyle.getPropertyValue("height")); + var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left")); + var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right")); + var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top")); + var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom")); + var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width")); + var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width")); + var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width")); + var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width")); + + //If box-sizing is border-box, we need to subtract padding and border. + var parentBoxSizing = computedStyle.getPropertyValue("box-sizing"); + if (parentBoxSizing == "border-box") { + innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight); + innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom); + } else { + innerWidth = width; + innerHeight = height; + } + var obj = {}; + obj["width"] = innerWidth; + obj["height"] = innerHeight; + return obj; +} + +export default function BigText(element, options) { + + if (typeof element === 'string') { + element = document.querySelector(element); + } else if (element.length) { + // Support for array based queries (such as jQuery) + element = element[0]; + } + + var defaultOptions = { + rotateText: null, + fontSizeFactor: 0.8, + maximumFontSize: null, + limitingDimension: "both", + horizontalAlign: "center", + verticalAlign: "center", + textAlign: "center", + whiteSpace: "nowrap" + }; + + //Merge provided options and default options + options = options || {}; + for (var opt in defaultOptions) + if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt)) + options[opt] = defaultOptions[opt]; + + //Get variables which we will reference frequently + var style = element.style; + var computedStyle = document.defaultView.getComputedStyle(element); + var parent = element.parentNode; + var parentStyle = parent.style; + var parentComputedStyle = document.defaultView.getComputedStyle(parent); + + //hides the element to prevent "flashing" + style.visibility = "hidden"; + + //Set some properties + style.display = "inline-block"; + style.clear = "both"; + style.float = "left"; + style.fontSize = (1000 * options.fontSizeFactor) + "px"; + style.lineHeight = "1000px"; + style.whiteSpace = options.whiteSpace; + style.textAlign = options.textAlign; + style.position = "relative"; + style.padding = 0; + style.margin = 0; + style.left = "50%"; + style.top = "50%"; + + //Get properties of parent to allow easier referencing later. + var parentPadding = { + top: parseInt(parentComputedStyle.getPropertyValue("padding-top")), + right: parseInt(parentComputedStyle.getPropertyValue("padding-right")), + bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")), + left: parseInt(parentComputedStyle.getPropertyValue("padding-left")), + }; + var parentBorder = { + top: parseInt(parentComputedStyle.getPropertyValue("border-top")), + right: parseInt(parentComputedStyle.getPropertyValue("border-right")), + bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")), + left: parseInt(parentComputedStyle.getPropertyValue("border-left")), + }; + + //Calculate the parent inner width and height + var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle); + var parentInnerWidth = parentInnerDimensions["width"]; + var parentInnerHeight = parentInnerDimensions["height"]; + + var box = { + width: element.offsetWidth, //Note: This is slightly larger than the jQuery version + height: element.offsetHeight, + }; + + + if (options.rotateText !== null) { + if (typeof options.rotateText !== "number") + throw "bigText error: rotateText value must be a number"; + var rotate = "rotate(" + options.rotateText + "deg)"; + style.webkitTransform = rotate; + style.msTransform = rotate; + style.MozTransform = rotate; + style.OTransform = rotate; + style.transform = rotate; + //calculating bounding box of the rotated element + var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180)); + var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180)); + box.width = element.offsetWidth * cosine + element.offsetHeight * sine; + box.height = element.offsetWidth * sine + element.offsetHeight * cosine; + } + + var widthFactor = (parentInnerWidth - parentPadding.left - parentPadding.right) / box.width; + var heightFactor = (parentInnerHeight - parentPadding.top - parentPadding.bottom) / box.height; + var lineHeight; + + if (options.limitingDimension.toLowerCase() === "width") { + lineHeight = Math.floor(widthFactor * 1000); + parentStyle.height = lineHeight + "px"; + } else if (options.limitingDimension.toLowerCase() === "height") { + lineHeight = Math.floor(heightFactor * 1000); + } else if (widthFactor < heightFactor) + lineHeight = Math.floor(widthFactor * 1000); + else if (widthFactor >= heightFactor) + lineHeight = Math.floor(heightFactor * 1000); + + var fontSize = lineHeight * options.fontSizeFactor; + if (options.maximumFontSize !== null && fontSize > options.maximumFontSize) { + fontSize = options.maximumFontSize; + lineHeight = fontSize / options.fontSizeFactor; + } + + style.fontSize = Math.floor(fontSize) + "px"; + style.lineHeight = Math.ceil(lineHeight) + "px"; + style.marginBottom = "0px"; + style.marginRight = "0px"; + + if (options.limitingDimension.toLowerCase() === "height") { + //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size + //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code + parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px"; + } + + //Calculate the inner width and height + var innerDimensions = _calculateInnerDimensions(computedStyle); + var innerWidth = innerDimensions["width"]; + var innerHeight = innerDimensions["height"]; + + switch (options.verticalAlign.toLowerCase()) { + case "top": + style.top = "0%"; + break; + case "bottom": + style.top = "100%"; + style.marginTop = Math.floor(-innerHeight) + "px"; + break; + default: + style.marginTop = Math.ceil((-innerHeight / 2)) + "px"; + break; + } + + switch (options.horizontalAlign.toLowerCase()) { + case "left": + style.left = "0%"; + break; + case "right": + style.left = "100%"; + style.marginLeft = Math.floor(-innerWidth) + "px"; + break; + default: + style.marginLeft = Math.ceil((-innerWidth / 2)) + "px"; + break; + } + + //shows the element after the work is done + style.visibility = "visible"; + + return element; +} diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss index 109a02df4..6a0d651d2 100644 --- a/src/client/views/nodes/LabelBox.scss +++ b/src/client/views/nodes/LabelBox.scss @@ -9,10 +9,10 @@ .labelBox-mainButton { max-width: 100%; - width: fit-content; - height: max-content; + width: 100%; + height: 100%; border-radius: inherit; - letter-spacing: 2px; + //letter-spacing: 2px; // bcz: doesn't work with LabelBigText text-transform: uppercase; overflow: hidden; display: inline-block; @@ -28,5 +28,5 @@ .labelBox-missingParam { width: 100%; background: lightgray; - border: dimGray solid 1px; + border: dimgray solid 1px; }
\ No newline at end of file diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index db1ae0537..90b9ce55d 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -1,19 +1,20 @@ -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { documentSchema } from '../../../fields/documentSchemas'; import { List } from '../../../fields/List'; import { createSchema, listSpec, makeInterface } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, StrCast } from '../../../fields/Types'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { StyleProp } from '../StyleProvider'; import { FieldView, FieldViewProps } from './FieldView'; +import BigText from './LabelBigText'; import './LabelBox.scss'; -import { StyleProp } from '../StyleProvider'; const LabelSchema = createSchema({}); @@ -37,7 +38,8 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro } getTitle() { - return this.props.label || ""; + return this.props.label ? this.props.label : this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) : + typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); } protected createDropTarget = (ele: HTMLDivElement) => { @@ -81,7 +83,7 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []); const missingParams = params?.filter(p => !this.paramsDoc[p]); params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ... - const label = this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); + const label = this.getTitle(); return ( <div className="labelBox-outerDiv" onMouseLeave={action(() => this._mouseOver = false)} @@ -90,17 +92,28 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> <div className="labelBox-mainButton" style={{ backgroundColor: this.hoverColor, - fontSize: StrCast(this.layoutDoc._fontSize) || "inherit", + fontSize: StrCast(this.layoutDoc._fontSize) || Math.min(18, this.props.PanelHeight() / 2), fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit", letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, - paddingLeft: NumCast(this.layoutDoc._xPadding), - paddingRight: NumCast(this.layoutDoc._xPadding), - paddingTop: NumCast(this.layoutDoc._yPadding), - paddingBottom: NumCast(this.layoutDoc._yPadding), + width: this.props.PanelWidth(), + height: this.props.PanelHeight(), whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap" }} > - {label.startsWith("#") ? (null) : label} + <span ref={r => { + if (r) { + BigText(r, { + rotateText: null, + fontSizeFactor: 1, + maximumFontSize: null, + limitingDimension: "both", + horizontalAlign: "center", + verticalAlign: "center", + textAlign: "center", + whiteSpace: "nowrap" + }); + } + }}>{label.startsWith("#") ? (null) : label}</span> </div> <div className="labelBox-fieldKeyParams" > {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)} diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 1e0172d24..a7bfb93eb 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -128,7 +128,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}> <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} /> {!this._forceOpen ? (null) : <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}> - <FontAwesomeIcon color="dimGray" icon={"times"} size={"sm"} /> + <FontAwesomeIcon color="dimgray" icon={"times"} size={"sm"} /> </div>} </div> ); diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 55ea45bb8..b82d16677 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,10 +2,10 @@ import React = require("react"); import { observer } from "mobx-react"; import { documentSchema } from "../../../fields/documentSchemas"; import { makeInterface } from "../../../fields/Schema"; -import { returnFalse } from "../../../Utils"; -import { CollectionTreeView } from "../collections/CollectionTreeView"; +import { emptyFunction, returnFalse } from "../../../Utils"; import { ViewBoxBaseComponent } from "../DocComponent"; import { StyleProp } from "../StyleProvider"; +import { ComparisonBox } from "./ComparisonBox"; import { FieldView, FieldViewProps } from './FieldView'; import "./LinkBox.scss"; @@ -20,22 +20,15 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>( if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true); return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} > - <CollectionTreeView {...this.props} - childDocuments={[this.dataDoc]} - treeViewOpen={true} - treeViewExpandedView={"fields"} - treeViewHideTitle={true} - treeViewSkipFields={["treeViewExpandedView", "aliases", "_removeDropProperties", - "treeViewOpen", "aliasNumber", "isPrototype", "creationDate", "author"]} + <ComparisonBox {...this.props} + fieldKey="anchor" + setHeight={emptyFunction} dontRegisterView={true} renderDepth={this.props.renderDepth + 1} - CollectionView={undefined} - isAnyChildContentActive={returnFalse} isContentActive={this.isContentActiveFunc} addDocument={returnFalse} removeDocument={returnFalse} - moveDocument={returnFalse}> - </CollectionTreeView> + moveDocument={returnFalse} /> </div>; } }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index b62a4dd56..a9d33f161 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,8 +1,9 @@ import React = require("react"); +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import "./LinkDescriptionPopup.scss"; -import { observable, action } from "mobx"; +import { Doc } from "../../../fields/Doc"; import { LinkManager } from "../../util/LinkManager"; +import "./LinkDescriptionPopup.scss"; import { TaskCompletionBox } from "./TaskCompletedBox"; @@ -25,7 +26,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { onDismiss = (add: boolean) => { LinkDescriptionPopup.descriptionPopup = false; if (add) { - LinkManager.currentLink && (LinkManager.currentLink.description = this.description); + LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description); } } diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 126a37eb8..424083dac 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from "mobx-react"; import wiki from "wikijs"; import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc"; -import { NumCast, StrCast } from "../../../fields/Types"; +import { NumCast, StrCast, Cast } from "../../../fields/Types"; import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from "../../documents/Documents"; @@ -14,6 +14,7 @@ import { undoBatch } from '../../util/UndoManager'; import { DocumentView, DocumentViewSharedProps } from "./DocumentView"; import './LinkDocPreview.scss'; import React = require("react"); +import { DocumentType } from '../../documents/DocumentTypes'; interface LinkDocPreviewProps { linkDoc?: Doc; @@ -85,7 +86,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._linkDoc = DocListCast(anchor.links)[0]; this._linkSrc = anchor; const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc); - this._targetDoc = linkTarget?.annotationOn as Doc ?? linkTarget; + this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; this._toolTipText = ""; } })); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 972dcc0be..d54b65d92 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,9 +3,9 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; -import { Doc, DocListCast, Opt, WidthSym } from "../../../fields/Doc"; +import { Doc, DocListCast, Opt, WidthSym, StrListCast } from "../../../fields/Doc"; import { documentSchema } from '../../../fields/documentSchemas'; -import { makeInterface } from "../../../fields/Schema"; +import { makeInterface, listSpec } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { PdfField } from "../../../fields/URLField"; import { TraceMobx } from '../../../fields/util'; @@ -25,6 +25,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; import React = require("react"); +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>; const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema); @@ -96,7 +97,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); } - public search = (string: string, fwd: boolean) => this._pdfViewer?.search(string, fwd); + public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { + if (!this._searching && !clear) { + this._searching = true; + setTimeout(() => { + this._searchRef.current?.focus(); + this._searchRef.current?.select(); + this._searchRef.current?.setRangeText(searchString); + }); + } + return this._pdfViewer?.search(searchString, bwd, clear) || false; + }); public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._curPage = (this.Document._curPage || 1) - 1; return true; }; @@ -107,11 +118,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onKeyDown = action((e: KeyboardEvent) => { let processed = false; switch (e.key) { - case "f": if (e.ctrlKey) { - setTimeout(() => this._searchRef.current?.focus(), 100); - this._searching = processed = true; - } - break; case "PageDown": processed = this.forwardPage(); break; case "PageUp": processed = this.backPage(); break; } @@ -186,8 +192,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> <button className="pdfBox-overlayButton" title={searchTitle} /> <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} - onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} /> - <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}> + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + <button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> <FontAwesomeIcon icon="search" size="sm" /> </button> <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} > @@ -198,7 +204,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </button> </div> <button className="pdfBox-overlayButton" title={searchTitle} - onClick={action(() => { this._searching = !this._searching; this.search("mxytzlaf", true); })} > + onClick={action(() => { this._searching = !this._searching; this.search("", true, true); })} > <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> @@ -246,9 +252,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } - contentScaling = () => { - return 1; - } + contentScaling = () => 1; @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index cdd36eb3b..4871599b8 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -14,6 +14,9 @@ height: 100%; position: relative; .videoBox-viewer { + display:flex; + flex-direction: column; + height: 100%; border-radius: inherit; opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger } @@ -26,7 +29,17 @@ .videoBox-stackPanel { z-index: -1; width: 100%; - position: absolute; + position: relative; + } + + .videoBox-annotationLayer { + position: relative; + transform-origin: left top; + top: 0; + width: 100%; + pointer-events: none; + mix-blend-mode: multiply; // bcz: makes text fuzzy! + overflow: hidden; } } diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 90de3227f..440ccf638 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -30,6 +30,7 @@ import { DragManager } from "../../util/DragManager"; import { DocumentManager } from "../../util/DocumentManager"; import { DocumentType } from "../../documents/DocumentTypes"; import { Tooltip } from "@material-ui/core"; +import { AnchorMenu } from "../pdf/AnchorMenu"; const path = require('path'); type VideoDocument = makeInterface<[typeof documentSchema]>; @@ -74,7 +75,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp getAnchor = () => { const timecode = Cast(this.layoutDoc._currentTimecode, "number", null); - return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined) || this.rootDoc; + const marquee = AnchorMenu.Instance.GetAnchor?.(); + return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc; } videoLoad = () => { @@ -548,7 +550,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } @computed get annotationLayer() { - return <div className="imageBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; + return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />; } marqueeDown = (e: React.PointerEvent) => { @@ -566,10 +568,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.props.select(true); }); + @computed get fitWidth() { return this.props.docViewPath?.().lastElement().fitWidth; } contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; scaling = () => this.props.scaling?.() || 1; - panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100; - panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100; + panelWidth = (): number => this.fitWidth ? this.props.PanelWidth() : (Doc.NativeAspect(this.rootDoc) || 1) * this.panelHeight(); + panelHeight = (): number => this.fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.heightPercent / 100 * this.props.PanelHeight(); screenToLocalTransform = () => { const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent); @@ -583,10 +586,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont} style={{ pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined, - borderRadius + borderRadius, + overflow: this.props.docViewPath?.().lastElement().fitWidth ? "auto" : undefined }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}> <div className="videoBox-viewer" onPointerDown={this.marqueeDown} > - <div style={{ position: "absolute", transition: this.transition, width: this.panelWidth(), height: this.panelHeight(), top: 0, left: `${(100 - this.heightPercent) / 2}%` }}> + <div style={{ + position: "absolute", transition: this.transition, + width: this.panelWidth(), + height: this.panelHeight(), + top: 0, + left: (this.props.PanelWidth() - this.panelWidth()) / 2 + }}> <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} @@ -606,9 +616,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.contentFunc} </CollectionFreeFormView> </div> - {this.uIButtons} {this.annotationLayer} - {this.renderTimeline} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) : <MarqueeAnnotator rootDoc={this.rootDoc} @@ -623,6 +631,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} />} + {this.renderTimeline} + {this.uIButtons} </div> </div >); } diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index 417a17d96..79289abaa 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -6,6 +6,92 @@ position: relative; display: flex; + .webBox-ui { + position: absolute; + width: 100%; + height: 100%; + z-index: 1; + pointer-events: none; + top: 0; + left: 0; + overflow: hidden; + + .webBox-overlayButton { + border-bottom-left-radius: 50%; + display: flex; + justify-content: space-evenly; + align-items: center; + height: 20px; + background: none; + padding: 0; + position: absolute; + pointer-events: all; + color: white; + bottom: 0; + right: 0; + + .webBox-overlayButton-arrow { + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-right: 15px solid #121721; + transition: all 0.5s; + } + + .webBox-overlayButton-iconCont { + background: #121721; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + } + } + + .webBox-nextIcon, + .webBox-prevIcon { + background: #121721; + color: white; + height: 20px; + width: 25px; + display: flex; + position: relative; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + padding: 0px; + } + + .webBox-overlayButton:hover { + background: none; + } + + + .webBox-overlayCont { + position: absolute; + width: calc(100% - 40px); + height: 20px; + background: #121721; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + transition: left .5s; + pointer-events: all; + + .webBox-searchBar { + width: 70%; + font-size: 14px; + } + } + } + .webBox-overlayButton-sidebar { background: #121721; height: 25px; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 19135b6dd..9956cc36b 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -16,9 +16,11 @@ import { TraceMobx } from "../../../fields/util"; import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { KeyCodes } from "../../util/KeyCodes"; import { Scripting } from "../../util/Scripting"; import { SnappingManager } from "../../util/SnappingManager"; import { undoBatch } from "../../util/UndoManager"; +import { MarqueeOptionsMenu } from "../collections/collectionFreeForm"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; @@ -30,6 +32,8 @@ import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { AnchorMenu } from "../pdf/AnchorMenu"; import { Annotation } from "../pdf/Annotation"; import { SidebarAnnos } from "../SidebarAnnos"; +import { StyleProp } from "../StyleProvider"; +import { DocumentViewProps } from "./DocumentView"; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from "./LinkDocPreview"; import "./WebBox.scss"; @@ -52,6 +56,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _keyInput = React.createRef<HTMLInputElement>(); private _initialScroll: Opt<number>; private _sidebarRef = React.createRef<SidebarAnnos>(); + private _searchRef = React.createRef<HTMLInputElement>(); + private _searchString = ""; + @observable private _webUrl = ""; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render. + @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled + @observable private _searching: boolean = false; + @observable private _showSidebar = false; @observable private _scrollTimer: any; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -69,12 +79,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps constructor(props: any) { super(props); - // if (true) {// his.webField) { - // Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); - // Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); - // } + Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850); + runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it. } + @action + search = (searchString: string, bwd?: boolean, clear: boolean = false) => { + if (!this._searching && !clear) { + this._searching = true; + setTimeout(() => { + this._searchRef.current?.focus(); + this._searchRef.current?.select(); + this._searchRef.current?.setRangeText(searchString); + }); + } + if (clear) { + this._iframe?.contentWindow?.getSelection()?.empty(); + } + if (searchString) { + (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true); + } + return true; + } async componentDidMount() { 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. @@ -130,6 +157,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this._iframe?.removeEventListener('wheel', this.iframeWheel, true); + this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp); } @action @@ -193,8 +221,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeUp = (e: PointerEvent) => { + this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. 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(); @@ -203,7 +231,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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) => { @@ -214,8 +242,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps MarqueeAnnotator.clearAnnotations(this._savedAnnotations); 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); + if (word || (e.target as any || "").className.includes("rangeslider") || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { 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._iframeClick = this._iframe ?? undefined; @@ -224,6 +251,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps e.stopPropagation(); e.preventDefault(); } + + // bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items. + if (e.button === 2 || (e.button === 0 && e.altKey)) { + e.preventDefault(); + e.stopPropagation(); + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(true); + } } getScrollHeight = () => this._scrollHeight; @@ -237,8 +272,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action iframeLoaded = (e: any) => { const iframe = this._iframe; + let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString()); + if (requrlraw !== this._url.toString()) { + if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { + const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); + const newsearch = matches?.lastElement()!; + if (matches) { + requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch)); + for (let i = 1; i < Array.from(matches)?.length; i++) { + requrlraw = requrlraw.replace(matches[i], ""); + } + } + requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", ""); + } + this.submitURL(requrlraw, undefined, true); + } if (iframe?.contentDocument) { - iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown); + iframe.contentDocument.addEventListener("pointerup", this.iframeUp); + 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); const initialScroll = this._initialScroll; @@ -248,13 +299,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._initialScroll = undefined; } iframe.setAttribute("enable-annotation", "true"); - iframe.contentDocument.addEventListener("click", undoBatch(action(e => { + iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => { let href = ""; - for (let ele = e.target; ele; ele = ele.parentElement) { + for (let ele = e.target as any; ele; ele = ele.parentElement) { href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href; } - if (href && this.webField?.origin) { - this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin)); + const origin = this.webField?.origin; + if (href && origin) { + e.stopPropagation(); + setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin))); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -305,8 +358,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (future.length) { + const curUrl = this._url; this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } return true; } return false; @@ -317,10 +377,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string")); const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []); if (history.length) { + const curUrl = this._url; if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]); else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]); this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); - console.log(this._urlHash); + if (this._webUrl === this._url) { + this._webUrl = curUrl; + setTimeout(action(() => this._webUrl = this._url)); + } else { + this._webUrl = this._url; + } return true; } return false; @@ -329,9 +395,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps static urlHash = (s: string) => { return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0)); } - @action - submitURL = (newUrl?: string, preview?: boolean) => { + submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => { if (!newUrl) return; if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl; try { @@ -343,7 +408,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this.layoutDoc._scrollTop = 0; future && (future.length = 0); } - if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); + if (!preview) { + this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl)); + !dontUpdateIframe && (this._webUrl = this._url); + } } catch (e) { console.log("WebBox URL error:" + this._url); } @@ -391,20 +459,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } - specificContextMenu = (e: React.MouseEvent): void => { + specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; - funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); - funcs.push({ - description: (!this.layoutDoc.allowReflow ? "Allow" : "Prevent") + " Reflow", event: () => { - const nw = !this.layoutDoc.allowReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); - this.layoutDoc.allowReflow = !nw; - if (nw) { - Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true); - } - }, icon: "snowflake" - }); - cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + if (!cm.findByDescription("Options...")) { + !Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" }); + funcs.push({ + description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => { + this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts; + if (this._iframe) { + runInAction(() => this._hackHide = true); + setTimeout(action(() => this._hackHide = false)); + } + }, icon: "snowflake" + }); + funcs.push({ + description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => { + const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1); + this.layoutDoc.forceReflow = !nw; + if (nw) { + Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true); + } + }, icon: "snowflake" + }); + cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); + } } @action @@ -417,26 +496,35 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false); } } - @action finishMarquee = (x?: number, y?: number) => { + @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { this._marqueeing = undefined; this._isAnnotating = false; this._iframeClick = undefined; - x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + if (x !== undefined && y !== undefined) { + this._setPreviewCursor?.(x, y, false, false); + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(false); + if (e?.button === 2 || e?.altKey) { + this.specificContextMenu(undefined as any); + this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y); + } + } } @computed get urlContent() { + if (this._hackHide) return (null); const field = this.dataDoc[this.props.fieldKey]; let view; if (field instanceof HtmlField) { view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url; + const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; 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"} />; + sandbox={`${this.layoutDoc.allowScripts ? "allow-scripts" : ""} 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"} style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly @@ -491,34 +579,63 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps NumCast(this.layoutDoc.nativeWidth) @computed get content() { - return <div className={"webBox-cont" + (!this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting ? "-interactive" : "")} - style={{ width: !this.layoutDoc.allowReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}> + const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; + return <div className={"webBox-cont" + (interactive ? "-interactive" : "")} + style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}> {this.urlContent} </div>; } @computed get annotationLayer() { TraceMobx(); + const pe = this.pointerEvents(); return <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> {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`} />) - } + <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)} </div>; } - @observable _showSidebar = false; @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; } + @computed get searchUI() { + return <div className="webBox-ui" + onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}> + <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> + <button className="webBox-overlayButton" title={"search"} /> + <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} + onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} /> + <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}> + <FontAwesomeIcon icon="search" size="sm" /> + </button> + </div> + <button className="webBox-overlayButton" title={"search"} + onClick={action(() => { this._searching = !this._searching; this.search("", false, true); })} > + <div className="webBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} /> + <div className="webBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}> + <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" /> + </div> + </button> + </div>; + } + searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value; showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno); setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func; 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); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; + basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")]; transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => { + if (doc instanceof Doc && property === StyleProp.PointerEvents) { + if (doc.textInlineAnnotations) return "none"; + } + return this.props.styleProvider?.(doc, props, property); + } + pointerEvents = () => this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none"; render() { - const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined; + const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : this.props.pointerEvents ? this.props.pointerEvents as any : undefined; const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.scaling?.() || 1); const renderAnnotations = (docFilters?: () => string[]) => @@ -533,7 +650,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ScreenToLocalTransform={this.scrollXf} scaling={returnOne} dropAction={"alias"} - docFilters={docFilters || this.props.docFilters} + docFilters={docFilters || this.basicFilter} dontRenderDocuments={docFilters ? false : true} select={emptyFunction} ContentScaling={returnOne} @@ -542,12 +659,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.sidebarAddDocument} - childPointerEvents={true} - pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; + styleProvider={this.childStyleProvider} + childPointerEvents={this.props.isContentActive() ? "all" : undefined} + pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />; return ( - <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.props.isContentActive() ? "all" : this.props.isContentActive() || SnappingManager.GetIsDragging() ? undefined : "none" }} > + <div className="webBox" ref={this._mainCont} + style={{ pointerEvents: this.pointerEvents() }} > <div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}> - <base target="_blank" /> <div className={"webBox-outerContent"} ref={this._outerRef} style={{ width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`, @@ -605,6 +723,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPointerDown={this.sidebarBtnDown} > <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" /> </div> + {!this.props.isContentActive() ? (null) : this.searchUI} </div>); } } diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 9d1ef937f..33fa23805 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -5,7 +5,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, StrListCast, WidthSym, HeightSym } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; import { createSchema, makeInterface } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; @@ -14,7 +14,6 @@ import { WebField } from '../../../../fields/URLField'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Scripting } from "../../../util/Scripting"; import { SelectionManager } from '../../../util/SelectionManager'; -import { ColorScheme } from '../../../util/SettingsManager'; import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { CollectionViewType } from '../../collections/CollectionView'; import { ContextMenu } from '../../ContextMenu'; @@ -114,7 +113,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon // Script for checking the outcome of the toggle const checkScript: string = StrCast(this.rootDoc.script) + "(0, true)"; - const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result; + const checkResult: number = ScriptField.MakeScript(checkScript)?.script.run().result || 0; if (numBtnType === NumButtonType.Slider) { @@ -159,7 +158,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon <div className={`menuButton ${this.type} ${numBtnType}`} > - <div className={`button`} onClick={action((e) => setValue(checkResult - 1))}> + <div className={`button`} onClick={action((e) => setValue(Number(checkResult) - 1))}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"minus"} /> </div> <div @@ -178,7 +177,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon onChange={action((e) => setValue(Number(e.target.value)))} /> </div> - <div className={`button`} onClick={action((e) => setValue(checkResult + 1))}> + <div className={`button`} onClick={action((e) => setValue(Number(checkResult) + 1))}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={"plus"} /> </div> @@ -262,8 +261,8 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking]; } else if (script === 'setFont') { - const selected = SelectionManager.Docs().lastElement(); - text = StrCast((selected?.type === DocumentType.RTF ? selected : Doc.UserDoc())._fontFamily); + const editorView = RichTextMenu.Instance?.TextView?.EditorView; + text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); noviceList = ["Roboto", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; } @@ -319,29 +318,35 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon ); } + @observable colorPickerClosed: boolean = true; + @computed get colorScript() { + const script = StrCast(this.rootDoc.script); + return ScriptField.MakeScript(script + '(colValue, checkResult)', { colValue: "string", checkResult: "boolean" }); + } + + colorPicker = (curColor: string) => { + const change = (value: ColorState) => { + const s = this.colorScript; + s && undoBatch(() => s.script.run({ colValue: Utils.colorString(value), checkResult: false }).result)(); + }; + const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', + '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', + '#FFFFFF', '#f1efeb', "transparent"]; + return <SketchPicker + onChange={change} + color={curColor} + presetColors={presets} />; + } /** * Color button */ @computed get colorButton() { - const active: string = StrCast(this.rootDoc.dropDownOpen); const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const curColor = this.colorScript?.script.run({ colValue: undefined, checkResult: true }).result ?? "transparent"; - const script: string = StrCast(this.rootDoc.script); - const scriptCheck: string = script + "(undefined, true)"; - const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; - - const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', - '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', - '#FFFFFF', '#f1efeb', "transparent"]; - - const colorBox = (func: (color: ColorState) => void) => <SketchPicker - disableAlpha={false} - onChange={func} - color={boolResult ? boolResult : "#FFFFFF"} - presetColors={colorOptions} />; const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> {this.label} </div>; @@ -350,30 +355,27 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" /> </div>; - - const click = (value: ColorState) => { - const s = ScriptField.MakeScript(script + '("' + Utils.colorString(value) + '", false)'); - s && undoBatch(() => s.script.run().result)(); - }; + setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return ( - <div className={`menuButton ${this.type} ${active}`} + <div className={`menuButton ${this.type} ${this.colorPickerClosed}`} style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen} + onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)} onPointerDown={e => e.stopPropagation()}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> - <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} /> + <div className="colorButton-color" style={{ backgroundColor: curColor }} /> {label} {/* {dropdownCaret} */} - {this.rootDoc.dropDownOpen ? + {this.colorPickerClosed ? (null) : <div> <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> - {colorBox(click)} + {this.colorPicker(curColor)} </div> - <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} /> - </div> - : null} + <div className="dropbox-background" onClick={action((e) => { + e.stopPropagation(); this.colorPickerClosed = true; + })} /> + </div>} </div> ); } @@ -388,7 +390,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon // Button label const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : - <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> + <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}> {this.label} </div>; @@ -407,7 +409,7 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon } else { return ( <div className={`menuButton ${this.type}`} - style={{ opacity: 1, backgroundColor: backgroundColor, color: color }}> + style={{ opacity: 1, backgroundColor, color }}> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} /> {label} </div> @@ -463,7 +465,6 @@ export class FontIconBox extends DocComponent<ButtonProps, FontIconDocument>(Fon render() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const dark: boolean = Doc.UserDoc().colorScheme === ColorScheme.Dark; const label = !this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}> @@ -573,15 +574,9 @@ Scripting.addGlobal(function setView(view: string) { Scripting.addGlobal(function setBackgroundColor(color?: string, checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); if (checkResult) { - if (selected) { - return selected._backgroundColor; - } else { - return "#FFFFFF"; - } + return selected?._backgroundColor ?? "transparent"; } - if (selected?.type === DocumentType.INK) selected.fillColor = color; if (selected) selected._backgroundColor = color; - Doc.UserDoc()._fontColor = color; }); // toggle: Set overlay status of selected document @@ -599,7 +594,7 @@ Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) { const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; if (checkResult && selected) { if (NumCast(selected.Document.z) >= 1) return Colors.MEDIUM_BLUE; - else return "transparent"; + return "transparent"; } selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("[FontIconBox.tsx] toggleOverlay failed"); }); @@ -620,16 +615,18 @@ Scripting.addGlobal(function toggleOverlay(checkResult?: boolean) { Scripting.addGlobal(function setFont(font: string, checkResult?: boolean) { SelectionManager.Docs().map(doc => doc._fontFamily = font); const editorView = RichTextMenu.Instance.TextView?.EditorView; - editorView?.state && RichTextMenu.Instance.setFontFamily(font, editorView); - Doc.UserDoc()._fontFamily = font; - return Doc.UserDoc()._fontFamily; + if (checkResult) { + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + } + if (editorView) RichTextMenu.Instance.setFontFamily(font); + else Doc.UserDoc().fontFamily = font; }); Scripting.addGlobal(function getActiveTextInfo(info: "family" | "size" | "color" | "highlight") { const editorView = RichTextMenu.Instance.TextView?.EditorView; const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection(); switch (info) { - case "family": return style?.activeColors[0]; + case "family": return style?.activeFamilies[0]; case "size": return style?.activeSizes[0]; case "color": return style?.activeColors[0]; case "highlight": return style?.activeHighlights[0]; @@ -646,7 +643,7 @@ Scripting.addGlobal(function setAlignment(align: "left" | "right" | "center", ch active = StrCast(Doc.UserDoc().textAlign); } if (active === align) return Colors.MEDIUM_BLUE; - else return "transparent"; + return "transparent"; } SelectionManager.Docs().map(doc => doc.textAlign = align); switch (align) { @@ -670,38 +667,25 @@ Scripting.addGlobal(function setBulletList(mapStyle: "bullet" | "decimal", check if (checkResult) { const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); if (active === mapStyle) return Colors.MEDIUM_BLUE; - else return "transparent"; + return "transparent"; } if (editorView) { const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle(); - if (active === mapStyle) { - editorView?.state && RichTextMenu.Instance.changeListType(editorView.state.schema.nodes.ordered_list.create({ mapStyle: "" })); - } else { - editorView?.state && RichTextMenu.Instance.changeListType(editorView.state.schema.nodes.ordered_list.create({ mapStyle: mapStyle })); - } + editorView?.state && RichTextMenu.Instance.changeListType( + editorView.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? "" : mapStyle })); } }); // toggle: Set overlay status of selected document Scripting.addGlobal(function setFontColor(color?: string, checkResult?: boolean) { - const selected = SelectionManager.Docs().lastElement(); const editorView = RichTextMenu.Instance.TextView?.EditorView; if (checkResult) { - if (selected) { - return selected._fontColor; - } else { - return Doc.UserDoc()._fontColor; - } + return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor; } - if (selected) { - selected._fontColor = color; - if (color) { - editorView?.state && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch); - } - } - Doc.UserDoc()._fontColor = color; + if (editorView) color && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch); + else Doc.UserDoc().fontColor = color; }); // toggle: Set overlay status of selected document @@ -710,11 +694,7 @@ Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: bool const editorView = RichTextMenu.Instance.TextView?.EditorView; if (checkResult) { - if (selected) { - return selected._fontHighlight; - } else { - return Doc.UserDoc()._fontHighlight; - } + return (selected ?? Doc.UserDoc())._fontHighlight; } if (selected) { selected._fontColor = color; @@ -725,62 +705,43 @@ Scripting.addGlobal(function setFontHighlight(color?: string, checkResult?: bool Doc.UserDoc()._fontHighlight = color; }); - - // toggle: Set overlay status of selected document -Scripting.addGlobal(function setFontSize(size: string, checkResult?: boolean) { +Scripting.addGlobal(function setFontSize(size: string | number, checkResult?: boolean) { + if (typeof size === "number") size = size.toString(); + if (size && Number(size).toString() === size) size += "px"; + const editorView = RichTextMenu.Instance.TextView?.EditorView; if (checkResult) { - const size: number = parseInt(StrCast(Doc.UserDoc()._fontSize), 10); - return size; + return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", ""); } - const editorView = RichTextMenu.Instance.TextView?.EditorView; - editorView?.state && RichTextMenu.Instance.setFontSize(Number(size), editorView); - Doc.UserDoc()._fontSize = size + "px"; + if (editorView) RichTextMenu.Instance.setFontSize(size); + else Doc.UserDoc()._fontSize = size; }); Scripting.addGlobal(function toggleBold(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (editorView) { - const bold: boolean = editorView?.state && RichTextMenu.Instance.getBoldStatus(); - if (bold) return Colors.MEDIUM_BLUE; - else return "transparent"; - } - else return "transparent"; + return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === "bold") ? Colors.MEDIUM_BLUE : "transparent"; } - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (editorView) { - editorView?.state && RichTextMenu.Instance.toggleBold(editorView, true); - } - SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.bold = !doc.bold); - Doc.UserDoc().bold = !Doc.UserDoc().bold; - return Doc.UserDoc().bold; + if (editorView) RichTextMenu.Instance?.toggleBold(); + else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === "bold" ? undefined : "bold"; }); Scripting.addGlobal(function toggleUnderline(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return "transparent"; + return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === "underline") ? Colors.MEDIUM_BLUE : "transparent"; } - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (editorView) { - editorView?.state && RichTextMenu.Instance.toggleUnderline(editorView, true); - } - SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.underline = !doc.underline); - Doc.UserDoc().underline = !Doc.UserDoc().underline; - return Doc.UserDoc().underline; + if (editorView) RichTextMenu.Instance?.toggleUnderline(); + else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === "underline" ? undefined : "underline"; }); Scripting.addGlobal(function toggleItalic(checkResult?: boolean) { + const editorView = RichTextMenu.Instance?.TextView?.EditorView; if (checkResult) { - return "transparent"; + return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === "italics") ? Colors.MEDIUM_BLUE : "transparent"; } - const editorView = RichTextMenu.Instance.TextView?.EditorView; - if (editorView) { - editorView?.state && RichTextMenu.Instance.toggleItalic(editorView, true); - } - SelectionManager.Docs().filter(doc => StrCast(doc.type) === DocumentType.RTF).map(doc => doc.italic = !doc.italic); - Doc.UserDoc().italic = !Doc.UserDoc().italic; - return Doc.UserDoc().italic; + if (editorView) RichTextMenu.Instance?.toggleItalics(); + else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === "italics" ? undefined : "italics"; }); @@ -794,24 +755,23 @@ Scripting.addGlobal(function toggleItalic(checkResult?: boolean) { Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolean) { if (checkResult) { - if (Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool) return Colors.MEDIUM_BLUE; - else return "transparent"; + return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ? + Colors.MEDIUM_BLUE : "transparent"; } - if (tool === "circle") { - Doc.UserDoc().activeInkTool = "pen"; - GestureOverlay.Instance.InkShape = tool; - } else if (tool === "square") { - Doc.UserDoc().activeInkTool = "pen"; - GestureOverlay.Instance.InkShape = tool; - } else if (tool === "line") { - Doc.UserDoc().activeInkTool = "pen"; - GestureOverlay.Instance.InkShape = tool; - } else if (tool) { - if (Doc.UserDoc().activeInkTool === tool && GestureOverlay.Instance.InkShape === "" || GestureOverlay.Instance.InkShape === tool) { - GestureOverlay.Instance.InkShape = ""; + if (["circle", "square", "line"].includes(tool)) { + if (GestureOverlay.Instance.InkShape === tool) { + Doc.UserDoc().activeInkTool = InkTool.None; + GestureOverlay.Instance.InkShape = InkTool.None; + } else { + Doc.UserDoc().activeInkTool = InkTool.Pen; + GestureOverlay.Instance.InkShape = tool; + } + } else if (tool) { // pen + if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) { Doc.UserDoc().activeInkTool = InkTool.None; } else { Doc.UserDoc().activeInkTool = tool; + GestureOverlay.Instance.InkShape = ""; } } else { Doc.UserDoc().activeInkTool = InkTool.None; @@ -823,7 +783,7 @@ Scripting.addGlobal(function setFillColor(color?: string, checkResult?: boolean) const selected = SelectionManager.Docs().lastElement(); if (checkResult) { if (selected?.type === DocumentType.INK) { - return StrCast(selected._backgroundColor); + return StrCast(selected.fillColor); } return ActiveFillColor(); } @@ -901,4 +861,17 @@ Scripting.addGlobal(function toggleSchemaPreview(checkResult?: boolean) { selected.schemaPreviewWidth = 0; } } +}); + +/** STACK + * groupBy + */ +Scripting.addGlobal(function setGroupBy(key: string, checkResult?: boolean) { + SelectionManager.Docs().map(doc => doc._fontFamily = key); + const editorView = RichTextMenu.Instance.TextView?.EditorView; + if (checkResult) { + return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); + } + if (editorView) RichTextMenu.Instance.setFontFamily(key); + else Doc.UserDoc().fontFamily = key; });
\ No newline at end of file diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 8915d7c47..364be461f 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,7 +1,7 @@ import { IReactionDisposer, reaction, observable, action } from "mobx"; import { NodeSelection } from "prosemirror-state"; import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc"; -import { Cast, StrCast } from "../../../../fields/Types"; +import { Cast, StrCast, NumCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, Utils, returnTransparent } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -12,6 +12,7 @@ import { FormattedTextBox } from "./FormattedTextBox"; import React = require("react"); import * as ReactDOM from 'react-dom'; import { observer } from "mobx-react"; +import { ColorScheme } from "../../../util/SettingsManager"; export class DashDocView { _fieldWrapper: HTMLSpanElement; // container for label and value @@ -20,7 +21,7 @@ export class DashDocView { this._fieldWrapper = document.createElement("span"); this._fieldWrapper.style.position = "relative"; this._fieldWrapper.style.textIndent = "0"; - this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard.darkScheme ? "dimGray" : "lightGray")); + this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "lightGray")); this._fieldWrapper.style.width = node.attrs.width; this._fieldWrapper.style.height = node.attrs.height; this._fieldWrapper.style.display = node.attrs.hidden ? "none" : "inline-block"; @@ -69,30 +70,31 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { @observable _finalLayout: any; @observable _resolvedDataDoc: any; - constructor(props: IDashDocViewInternal) { - super(props); - this._textBox = this.props.tbox; - const updateDoc = action((dashDoc: Doc) => { - this._dashDoc = dashDoc; - this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); + updateDoc = action((dashDoc: Doc) => { + this._dashDoc = dashDoc; + this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey); - if (this._finalLayout) { - if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { - this._finalLayout.rootDocument = dashDoc.aliasOf; - } - this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null); + if (this._finalLayout) { + if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { + this._finalLayout.rootDocument = dashDoc.aliasOf; } - if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") { - try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made - this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { - ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" - })); - } catch (e) { - console.log("DashDocView:" + e); - } + this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null); + } + if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") { + try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made + this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { + ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" + })); + } catch (e) { + console.log("DashDocView:" + e); } - }); + } + }); + + constructor(props: IDashDocViewInternal) { + super(props); + this._textBox = this.props.tbox; DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { @@ -101,15 +103,27 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias); aliasedDoc.layoutKey = "layout"; this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined); - updateDoc(aliasedDoc); + this.updateDoc(aliasedDoc); } }); } else { - updateDoc(dashDoc); + this.updateDoc(dashDoc); } }); } + componentDidMount() { + this._disposers.upater = reaction(() => this._dashDoc && (NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width)), + () => { + if (this._dashDoc) { + this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { + ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px" + })); + } + }); + } + + removeDoc = () => { this.props.view.dispatch(this.props.view.state.tr .setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos()))) diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index e7dd286a5..c36e6804b 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -8,7 +8,7 @@ height: 10px; position: relative; display: inline-block; - background: dimGray; + background: dimgray; } .dashFieldView-fieldCheck { min-width: 12px; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 0d38bd5b8..cfbd1962e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -280,8 +280,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()))); if ((!curTemp && !curProto) || curText || json.includes("dash")) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (removeSelection(json) !== removeSelection(curLayout?.Data)) { - !curText && tx.storedMarks?.filter(m => m.type.name === "pFontSize").map(m => Doc.UserDoc().fontSize = this.layoutDoc._fontSize = (m.attrs.fontSize + "px")); - !curText && tx.storedMarks?.filter(m => m.type.name === "pFontFamily").map(m => Doc.UserDoc().fontFamily = this.layoutDoc._fontFamily = m.attrs.fontFamily); this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText); this.dataDoc[this.props.fieldKey + "-noTemplate"] = true;//(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); @@ -463,10 +461,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const target = dragData.droppedDocuments[0]; target._fitToBox = true; const node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), height: target[HeightSym](), + width: target[WidthSym](), + height: target[HeightSym](), title: "dashDoc", docid: target[Id], - float: "right" + float: "unset" }); const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); @@ -833,7 +832,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { - autoHeight && this.props.setHeight(marginsHeight + Math.max(sidebarHeight, textHeight)); + autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight)); }, { fireImmediately: true }); this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { @@ -894,11 +893,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false }); this._disposers.selected = reaction(() => this.props.isSelected(), - action((selected) => { + action(selected => { if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } - })); + if (this._editorView && selected) { + RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + } + }), { fireImmediately: true }); if (!this.props.dontRegisterView) { this._disposers.record = reaction(() => this._recording, @@ -959,7 +961,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp GoogleApiClientUtils.Docs.write({ reference, content, mode }); } }; - UndoManager.AddEvent({ undo, redo }); + UndoManager.AddEvent({ undo, redo, prop: "" }); redo(); }); } @@ -1168,7 +1170,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp selectOnLoad && this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) { - this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })]; + this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []), + schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }), + ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []), + ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])]; } } @@ -1187,9 +1196,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if ((e.target as any).tagName === "AUDIOTAG") { e.preventDefault(); e.stopPropagation(); + const timecode = Number((e.target as any)?.dataset?.timecode); DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { if (anchor instanceof Doc) { - const timecode = NumCast(anchor.timecodeToShow, 0); + // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; const func = () => { const docView = DocumentManager.Instance.getDocumentView(audiodoc); @@ -1468,19 +1478,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } tryUpdateScrollHeight = () => { - if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children) { - const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; - if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { - setScrollHeight(); - } else { - setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... - } + const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; + if (children) { + const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation + const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; + if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { + setScrollHeight(); + } else { + setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... } } } @@ -1601,7 +1609,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), - fontWeight: Cast(this.layoutDoc._fontWeight, "number", null), + fontWeight: Cast(this.layoutDoc._fontWeight, "string", null) as any, fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"), pointerEvents: interactive ? undefined : "none", }} diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 9904a7939..bd05af977 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -1,8 +1,7 @@ import React = require("react"); -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { lift, wrapIn } from "prosemirror-commands"; import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model"; @@ -10,10 +9,7 @@ import { wrapInList } from "prosemirror-schema-list"; import { EditorState, NodeSelection, TextSelection } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Doc } from "../../../../fields/Doc"; -import { DarkPastelSchemaPalette, PastelSchemaPalette } from '../../../../fields/SchemaHeaderField'; import { Cast, StrCast } from "../../../../fields/Types"; -import { TraceMobx } from "../../../../fields/util"; -import { unimplementedFunction, Utils } from "../../../../Utils"; import { DocServer } from "../../../DocServer"; import { LinkManager } from "../../../util/LinkManager"; import { SelectionManager } from "../../../util/SelectionManager"; @@ -29,7 +25,7 @@ const { toggleMark } = require("prosemirror-commands"); @observer export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { - static Instance: RichTextMenu; + @observable static Instance: RichTextMenu; public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef<HTMLInputElement>(); @@ -39,22 +35,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { public _brushMap: Map<string, Set<Mark>> = new Map(); @observable private collapsed: boolean = false; - @observable private boldActive: boolean = false; - @observable private italicsActive: boolean = false; - @observable private underlineActive: boolean = false; - @observable private strikethroughActive: boolean = false; - @observable private subscriptActive: boolean = false; - @observable private superscriptActive: boolean = false; - - @observable private activeFontSize: string = ""; - @observable private activeFontFamily: string = ""; + @observable private _boldActive: boolean = false; + @observable private _italicsActive: boolean = false; + @observable private _underlineActive: boolean = false; + @observable private _strikethroughActive: boolean = false; + @observable private _subscriptActive: boolean = false; + @observable private _superscriptActive: boolean = false; + + @observable private _activeFontSize: string = "13px"; + @observable private _activeFontFamily: string = ""; @observable private activeListType: string = ""; @observable private activeAlignment: string = "left"; @observable private brushMarks: Set<Mark> = new Set(); @observable private showBrushDropdown: boolean = false; - @observable private activeFontColor: string = "black"; + @observable private _activeFontColor: string = "black"; @observable private showColorDropdown: boolean = false; @observable private activeHighlightColor: string = "transparent"; @@ -67,10 +63,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { _delayHide = false; constructor(props: Readonly<{}>) { super(props); - RichTextMenu.Instance = this; - this._canFade = false; - //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); - runInAction(() => this.Pinned = true); + runInAction(() => { + RichTextMenu.Instance = this; + this._canFade = false; + //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]); + this.Pinned = true; + }); } componentDidMount() { @@ -81,6 +79,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this._reaction?.(); } + @computed get bold() { return this._boldActive; } + @computed get underline() { return this._underlineActive; } + @computed get italics() { return this._italicsActive; } + @computed get strikeThrough() { return this._strikethroughActive; } + @computed get fontColor() { return this._activeFontColor; } + @computed get fontFamily() { return this._activeFontFamily; } + @computed get fontSize() { return this._activeFontSize; } + public delayHide = () => this._delayHide = true; @action @@ -110,10 +116,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this.activeAlignment = this.getActiveAlignment(); - this.activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; - this.activeFontSize = !activeSizes.length ? "13px" : activeSizes.length === 1 ? String(activeSizes[0]) : "..."; - this.activeFontColor = !activeColors.length ? "black" : activeColors.length === 1 ? String(activeColors[0]) : "..."; - this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length === 1 ? String(activeHighlights[0]) : "..."; + this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various"; + this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0]; + this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "..."; + this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length > 0 ? String(activeHighlights[0]) : "..."; // update link in current selection this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); @@ -125,7 +131,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (node?.type === schema.nodes.ordered_list) { let attrs = node.attrs; if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family }; - if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: `${mark.attrs.fontSize}px` }; + if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize }; if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color }; const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema); dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from)))); @@ -142,17 +148,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } } - getBoldStatus() { - if (this.view && this.TextView.props.isSelected(true)) { - const path = (this.view.state.selection.$from as any).path; - for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) { - if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) { - return path[i].attrs.strong; - } - } - } - } - // finds font sizes and families in selection getActiveAlignment() { if (this.view && this.TextView.props.isSelected(true)) { @@ -193,25 +188,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (this.TextView.props.isSelected(true)) { const state = this.view.state; const pos = this.view.state.selection.$from; - const ref_node = this.reference_node(pos); - if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) { - const marks = Array.from(ref_node.marks); - marks.push(...(this.view.state.storedMarks as any)); - marks.forEach(m => { - m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); - m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); - m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "px"); - m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); + const marks: Mark<any>[] = [...(state.storedMarks ?? [])]; + if (state.selection.empty) { + const ref_node = this.reference_node(pos); + marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : [])); + } else { + state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => { + node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } - !activeFamilies.length && (activeFamilies.push(StrCast(this.TextView.layoutDoc._fontFamily, StrCast(Doc.UserDoc().fontFamily)))); - !activeSizes.length && (activeSizes.push(StrCast(this.TextView.layoutDoc._fontSize, StrCast(Doc.UserDoc().fontSize)))); - !activeColors.length && (activeColors.push(StrCast(this.TextView.layoutDoc.color, StrCast(Doc.UserDoc().fontColor)))); + marks.forEach(m => { + m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family); + m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color); + m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize); + m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight)); + }); } - !activeFamilies.length && (activeFamilies.push(StrCast(Doc.UserDoc().fontFamily))); - !activeSizes.length && (activeSizes.push(StrCast(Doc.UserDoc().fontSize))); - !activeColors.length && (activeColors.push(StrCast(Doc.UserDoc().fontColor, "black"))); - !activeHighlights.length && (activeHighlights.push(StrCast(Doc.UserDoc().fontHighlight, ""))); return { activeFamilies, activeSizes, activeColors, activeHighlights }; } @@ -251,11 +243,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { return []; } activeMarks = markGroup.filter(mark_type => { - if (mark_type === state.schema.marks.pFontSize) { - return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); - } + // if (mark_type === state.schema.marks.pFontSize) { + // return mark.isINSet + // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name); + // } const mark = state.schema.mark(mark_type); - return ref_node.marks.includes(mark); + return mark.isInSet(ref_node.marks); }); } } @@ -270,56 +263,66 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; - this.boldActive = false; - this.italicsActive = false; - this.underlineActive = false; - this.strikethroughActive = false; - this.subscriptActive = false; - this.superscriptActive = false; + this._boldActive = false; + this._italicsActive = false; + this._underlineActive = false; + this._strikethroughActive = false; + this._subscriptActive = false; + this._superscriptActive = false; activeMarks.forEach(mark => { switch (mark.name) { - case "strong": this.boldActive = true; break; - case "em": this.italicsActive = true; break; - case "underline": this.underlineActive = true; break; - case "strikethrough": this.strikethroughActive = true; break; - case "subscript": this.subscriptActive = true; break; - case "superscript": this.superscriptActive = true; break; + case "strong": this._boldActive = true; break; + case "em": this._italicsActive = true; break; + case "underline": this._underlineActive = true; break; + case "strikethrough": this._strikethroughActive = true; break; + case "subscript": this._subscriptActive = true; break; + case "superscript": this._superscriptActive = true; break; } }); } - toggleBold = (view: EditorView, forceBool?: boolean) => { - const mark = view.state.schema.mark(view.state.schema.marks.strong, { strong: forceBool }); - this.setMark(mark, view.state, view.dispatch, false); - view.focus(); + toggleBold = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); + } } - toggleUnderline = (view: EditorView, forceBool?: boolean) => { - const mark = view.state.schema.mark(view.state.schema.marks.underline, { underline: forceBool }); - this.setMark(mark, view.state, view.dispatch, false); - view.focus(); + toggleUnderline = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.underline); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); + } } - toggleItalic = (view: EditorView, forceBool?: boolean) => { - const mark = view.state.schema.mark(view.state.schema.marks.em, { em: forceBool }); - this.setMark(mark, view.state, view.dispatch, false); - view.focus(); + toggleItalics = () => { + if (this.view) { + const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); + this.setMark(mark, this.view.state, this.view.dispatch, false); + this.view.focus(); + } } - setFontSize = (size: number, view: EditorView) => { - const fmark = view.state.schema.marks.pFontSize.create({ fontSize: size }); - this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); - view.focus(); - this.updateMenu(view, undefined, this.props); + setFontSize = (fontSize: string) => { + if (this.view) { + const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize }); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); + } } - setFontFamily = (family: string, view: EditorView) => { - const fmark = view.state.schema.marks.pFontFamily.create({ family: family }); - this.setMark(fmark, view.state, (tx: any) => view.dispatch(tx.addStoredMark(fmark)), true); - view.focus(); - this.updateMenu(view, undefined, this.props); + setFontFamily = (family: string) => { + if (this.view) { + const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); + this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); + this.view.focus(); + this.updateMenu(this.view, undefined, this.props); + } } setHighlight(color: String, view: EditorView, dispatch: any) { @@ -330,13 +333,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { } setColor(color: String, view: EditorView, dispatch: any) { - const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); - if (view.state.selection.empty) { - dispatch(view.state.tr.addStoredMark(colorMark)); - return false; + if (this.view) { + const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color }); + this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); + view.focus(); + this.updateMenu(this.view, undefined, this.props); } - this.setMark(colorMark, view.state, dispatch, true); - view.focus(); } // TODO: remove doesn't work diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 3fd7d61fa..711136469 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -282,7 +282,7 @@ export class RichTextRules { if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500, }, docid); + const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid); DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined); const fstate = this.TextBox.EditorView?.state; @@ -290,7 +290,7 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); } }); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); } return state.tr; } diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 655ee7e44..6103a28d6 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -61,13 +61,13 @@ export const marks: { [index: string]: MarkSpec } = { /** FONT SIZES */ pFontSize: { - attrs: { fontSize: { default: 10 } }, + attrs: { fontSize: { default: "10px" } }, parseDOM: [{ tag: "span", getAttrs(dom: any) { - return { fontSize: dom.style.fontSize ? Number(dom.style.fontSize.replace("px", "")) : "" }; + return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : "" }; } }], - toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize}px;` }] : ['span', 0] + toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize};` }] : ['span', 0] }, /* FONTS */ diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index f34b4a8ac..14d6e8be6 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -30,8 +30,7 @@ import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentView"; import { FieldView, FieldViewProps } from '../FieldView'; import "./PresBox.scss"; -import Color = require("color"); -import { PresEffect, PresStatus, PresMovement } from "./PresEnums"; +import { PresEffect, PresMovement, PresStatus } from "./PresEnums"; export class PinProps { audioRange?: boolean; @@ -411,7 +410,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) { LightboxView.SetLightboxDoc(undefined); //awaiting jump so that new scale can be found, since jumping is async - await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right + await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right } // After navigating to the document, if it is added as a presPinView then it will // adjust the pan and scale to that of the pinView when it was added. @@ -1036,6 +1035,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS); } + // Converts seconds to ms and updates presTransition + setZoom = (number: String, change?: number) => { + let scale = Number(number) / 100; + if (change) scale += change; + if (scale < 0.01) scale = 0.01; + if (scale > 1.5) scale = 1.5; + Array.from(this._selectedArray.keys()).forEach((doc) => doc.presZoom = scale); + } + // Converts seconds to ms and updates presDuration setDurationTime = (number: String, change?: number) => { let timeInMS = Number(number) * 1000; @@ -1153,11 +1161,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> @computed get transitionDropdown() { const activeItem: Doc = this.activeItem; const targetDoc: Doc = this.targetDoc; - const type = targetDoc.type; const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection); const isPinWithView: boolean = BoolCast(activeItem.presPinView); if (activeItem && targetDoc) { + const type = targetDoc.type; const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5; + const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75; let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2; if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration); const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None'; @@ -1182,6 +1191,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema> </div> </div> } + <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> + <div className="presBox-subheading">Zoom (% screen filled)</div> + <div className="ribbon-property"> + <input className="presBox-input" + type="number" value={zoom} + onChange={action((e) => this.setZoom(e.target.value))} />% + </div> + <div className="ribbon-propertyUpDown"> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> + <FontAwesomeIcon icon={"caret-up"} /> + </div> + <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}> + <FontAwesomeIcon icon={"caret-down"} /> + </div> + </div> + </div> + <input type="range" step="1" min="0" max="150" value={zoom} + className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`} + id="toolbar-slider" + onPointerDown={() => this._batch = UndoManager.StartBatch("presZoom")} + onPointerUp={() => this._batch?.end()} + onChange={(e: React.ChangeEvent<HTMLInputElement>) => { + e.stopPropagation(); + this.setZoom(e.target.value); + }} /> <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}> <div className="presBox-subheading">Movement Speed</div> <div className="ribbon-property"> |
