diff options
Diffstat (limited to 'src')
31 files changed, 566 insertions, 532 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5a7894c08..bb12ce568 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1054,8 +1054,8 @@ export namespace Docs { return inst; } - export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id); + export function WebanchorDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id); } export function CollectionAnchorDocument(options: DocumentOptions = {}, id?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 80a5f0993..fbfe306a4 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -669,7 +669,10 @@ export class CurrentUserUtils { } static schemaTools():Button[] { - return [{ title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'}, }]; + return [ + {title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} }, + {title: "Single Lines", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, buttonText: "Single Line", icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, + ]; } static webTools() { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 3a192f712..695b003a6 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,4 +1,4 @@ -import { action, observable, ObservableSet } from 'mobx'; +import { action, computed, observable, ObservableSet } from 'mobx'; import { AnimationSym, Doc, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; @@ -10,6 +10,7 @@ import { TabDocView } from '../views/collections/TabDocView'; import { LightboxView } from '../views/LightboxView'; import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox'; +import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { PresBox } from '../views/nodes/trails'; import { ScriptingGlobals } from './ScriptingGlobals'; @@ -18,10 +19,13 @@ const { Howl } = require('howler'); export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) - @observable public DocumentViews = new Set<DocumentView>(); + @observable _documentViews = new Set<DocumentView>(); @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; + @computed public get DocumentViews() { + return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox)); + } private static _instance: DocumentManager; public static get Instance(): DocumentManager { @@ -78,7 +82,7 @@ export class DocumentManager { // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { - this.DocumentViews.add(view); + this._documentViews.add(view); } this.callAddViewFuncs(view); }; @@ -96,7 +100,7 @@ export class DocumentManager { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { - this.DocumentViews.delete(view); + this._documentViews.delete(view); } SelectionManager.DeselectView(view); }); @@ -104,13 +108,13 @@ export class DocumentManager { //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; - Array.from(DocumentManager.Instance.DocumentViews).map(view => { + DocumentManager.Instance.DocumentViews.forEach(view => { if (view.rootDoc[Id] === id) { toReturn.push(view); } }); if (toReturn.length === 0) { - Array.from(DocumentManager.Instance.DocumentViews).map(view => { + DocumentManager.Instance.DocumentViews.forEach(view => { const doc = view.rootDoc.proto; if (doc && doc[Id] && doc[Id] === id) { toReturn.push(view); @@ -133,7 +137,7 @@ export class DocumentManager { // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href) // )?.rootDoc ?? toFind; - const docViewArray = Array.from(DocumentManager.Instance.DocumentViews); + const docViewArray = DocumentManager.Instance.DocumentViews; const passes = !doc ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined]; return passes.reduce( (pass, toReturn) => @@ -146,7 +150,7 @@ export class DocumentManager { public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const views: DocumentView[] = []; - Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); + DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined); }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { @@ -164,8 +168,8 @@ export class DocumentManager { toFindIn; const toReturn: DocumentView[] = []; - const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); - const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); + const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); + const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); // heuristic to return the "best" documents first: // choose a document in the lightbox first diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index f17a98616..70c2e3842 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,8 +7,6 @@ import * as typescriptlib from '!!raw-loader!./type_decls.d'; import * as ts from 'typescript'; import { Doc, Field } from '../../fields/Doc'; -import { ToScriptString } from '../../fields/FieldSymbols'; -import { ObjectField } from '../../fields/ObjectField'; import { RefField } from '../../fields/RefField'; import { ScriptField } from '../../fields/ScriptField'; import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; @@ -60,7 +58,14 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; // let params: any[] = [Docs, ...fieldTypes]; - const compiledFunction = new Function(...paramNames, `return ${script}`); + const compiledFunction = (() => { + try { + return new Function(...paramNames, `return ${script}`); + } catch { + return undefined; + } + })(); + if (!compiledFunction) return { compiled: false, errors }; const { capturedVariables = {} } = options; const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; @@ -155,7 +160,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void; export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { transformer: ts.TransformerFactory<ts.SourceFile>; - getVars?: () => { capturedVariables: { [name: string]: Field } }; + getVars?: () => { [name: string]: Field }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value @@ -205,16 +210,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if (options.transformer) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); const result = ts.transform(sourceFile, [options.transformer.transformer]); - if (options.transformer.getVars) { - const newCaptures = options.transformer.getVars(); + const newCaptures = options.transformer.getVars?.(); + if (Object.keys(newCaptures ?? {}).length) { // tslint:disable-next-line: prefer-object-spread - options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any; + //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any; + + const transformed = result.transformed; + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }); + script = printer.printFile(transformed[0]); } - const transformed = result.transformed; - const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed, - }); - script = printer.printFile(transformed[0]); result.dispose(); } const paramNames: string[] = []; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 85d36dbf8..26b64bfdd 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -867,11 +867,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> - <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}> - {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} - {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} + <div className="documentDecorations-topbar" style={{ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined }} onPointerDown={this.onContainerDown}> + {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} + {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} {hideTitle ? null : titleArea} - {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} + {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} </div> {hideResizers ? null : ( <> diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index ed7ec9dc1..0955ba8ff 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -10,12 +10,14 @@ .editableView-container-editing-oneLine { width: 100%; + height: max-content; span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; + p { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + } } input { @@ -33,4 +35,3 @@ border: none; outline: none; } -
\ No newline at end of file diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index d1311a60a..6b4132814 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,9 +1,11 @@ import React = require('react'); -import { action, observable } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; +import { DocumentIconContainer } from './nodes/DocumentIcon'; +import { OverlayView } from './OverlayView'; export interface EditableProps { /** @@ -37,7 +39,8 @@ export interface EditableProps { onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void; autosuggestProps: Autosuggest.AutosuggestProps<string, any>; }; - oneLine?: boolean; + oneLine?: boolean; // whether to display the editable view as a single input line or as a textarea + allowCRs?: boolean; // can carriage returns be entered editing?: boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; @@ -45,6 +48,7 @@ export interface EditableProps { showMenuOnLoad?: boolean; background?: string | undefined; placeholder?: string; + wrap?: string; // nowrap, pre-wrap, etc } /** @@ -55,7 +59,9 @@ export interface EditableProps { @observer export class EditableView extends React.Component<EditableProps> { private _ref = React.createRef<HTMLDivElement>(); - private _inputref = React.createRef<HTMLInputElement>(); + private _inputref: HTMLInputElement | HTMLTextAreaElement | null = null; + _overlayDisposer?: () => void; + _editingDisposer?: IReactionDisposer; @observable _editing: boolean = false; constructor(props: EditableProps) { @@ -63,21 +69,53 @@ export class EditableView extends React.Component<EditableProps> { this._editing = this.props.editing ? true : false; } + componentDidMount(): void { + this._editingDisposer = reaction( + () => this._editing, + editing => { + if (editing) { + setTimeout(() => { + if (this._inputref?.value.startsWith('=') || this._inputref?.value.startsWith(':=')) { + this._overlayDisposer?.(); + this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); + } + }); + } else { + this._overlayDisposer?.(); + this._overlayDisposer = undefined; + } + }, + { fireImmediately: true } + ); + } + @action componentDidUpdate() { if (this._editing && this.props.editing === false) { - this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } else if (this.props.editing !== undefined) { this._editing = this.props.editing; } } componentWillUnmount() { - this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + this._overlayDisposer?.(); + this._editingDisposer?.(); + this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } + onChange = (e: React.ChangeEvent) => { + const targVal = (e.target as any).value; + if (!(targVal.startsWith(':=') || targVal.startsWith('='))) { + this._overlayDisposer?.(); + this._overlayDisposer = undefined; + } else if (!this._overlayDisposer) { + this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); + } + }; + @action - onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { switch (e.key) { case 'Tab': e.stopPropagation(); @@ -89,13 +127,15 @@ export class EditableView extends React.Component<EditableProps> { if (!e.currentTarget.value) this.props.OnEmpty?.(); break; case 'Enter': - e.stopPropagation(); - if (!e.ctrlKey) { - this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); - } else if (this.props.OnFillDown) { - this.props.OnFillDown(e.currentTarget.value); - this._editing = false; - this.props.isEditingCallback?.(false); + if (this.props.allowCRs !== true) { + e.stopPropagation(); + if (!e.ctrlKey) { + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); + } else if (this.props.OnFillDown) { + this.props.OnFillDown(e.currentTarget.value); + this._editing = false; + this.props.isEditingCallback?.(false); + } } break; case 'Escape': @@ -103,9 +143,6 @@ export class EditableView extends React.Component<EditableProps> { this._editing = false; this.props.isEditingCallback?.(false); break; - case ':': - this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); - break; case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': @@ -117,6 +154,12 @@ export class EditableView extends React.Component<EditableProps> { case 'Meta': case 'Control': break; + case ':': + if (this.props.menuCallback) { + this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; + } + default: if (this.props.textCallback?.(e.key)) { this._editing = false; @@ -186,15 +229,32 @@ export class EditableView extends React.Component<EditableProps> { onChange: this.props.autosuggestProps.onChange, }} /> - ) : ( + ) : this.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? ( <input className="editableView-input" - ref={this._inputref} + ref={r => (this._inputref = r)} style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} placeholder={this.props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} autoFocus={true} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyPress={this.stopPropagation} + onPointerDown={this.stopPropagation} + onClick={this.stopPropagation} + onPointerUp={this.stopPropagation} + /> + ) : ( + <textarea + className="editableView-input" + ref={r => (this._inputref = r)} + style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }} + placeholder={this.props.placeholder} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} + defaultValue={this.props.GetValue()} + autoFocus={true} + onChange={this.onChange} onKeyDown={this.onKeyDown} onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} @@ -220,7 +280,7 @@ export class EditableView extends React.Component<EditableProps> { <div className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`} ref={this._ref} - style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }} + style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: this.props.oneLine ? 'nowrap' : 'pre-line', height: this.props.height, maxHeight: this.props.maxHeight }} //onPointerDown={this.stopPropagation} onClick={this.onClick} placeholder={this.props.placeholder}> diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 8fd2b87cc..556d9b6a0 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -8,14 +8,13 @@ import { GetEffectiveAcl } from '../../fields/util'; import { unimplementedFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; +import { FollowLinkScript } from '../util/LinkFollower'; import { undoBatch, UndoManager } from '../util/UndoManager'; import './MarqueeAnnotator.scss'; import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { AnchorMenu } from './pdf/AnchorMenu'; import React = require('react'); -import { ScriptField } from '../../fields/ScriptField'; -import { FollowLinkScript } from '../util/LinkFollower'; const _global = (window /* browser */ || global) /* node */ as any; export interface MarqueeAnnotatorProps { diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss index 778e9c445..adc82238e 100644 --- a/src/client/views/ScriptingRepl.scss +++ b/src/client/views/ScriptingRepl.scss @@ -31,6 +31,9 @@ border-radius: 25%; padding: 2px; } +.documentIcon-outerDiv:hover { + opacity: 0.3; +} .scriptingObject-icon { padding: 3px; @@ -48,4 +51,4 @@ .scriptingObject-leaf { margin-left: 15px; -}
\ No newline at end of file +} diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 4fecfa4d9..5dfe10def 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -10,34 +10,49 @@ import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @observer -export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { +export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void; value: { [key: string]: any }; name?: string }> { @observable collapsed = true; @action toggle = () => { this.collapsed = !this.collapsed; this.props.scrollToBottom(); - } + }; render() { const val = this.props.value; const proto = Object.getPrototypeOf(val); const name = (proto && proto.constructor && proto.constructor.name) || String(val); - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); if (this.collapsed) { return ( <div className="scriptingObject-collapsed"> - <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length}) + <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"> + <FontAwesomeIcon icon="caret-right" size="sm" /> + </span> + {title} (+{Object.keys(val).length}) </div> ); } else { return ( <div className="scriptingObject-open"> <div> - <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title} + <span onClick={this.toggle} className="scriptingObject-icon"> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </span> + {title} </div> <div className="scriptingObject-fields"> - {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)} + {Object.keys(val).map(key => ( + <ScriptingValueDisplay {...this.props} name={key} /> + ))} </div> </div> ); @@ -46,18 +61,32 @@ export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () } @observer -export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> { +export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void; value: any; name?: string }> { render() { const val = this.props.name ? this.props.value[this.props.name] : this.props.value; - if (typeof val === "object") { + if (typeof val === 'object') { return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />; - } else if (typeof val === "function") { - const name = "[Function]"; - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + } else if (typeof val === 'function') { + const name = '[Function]'; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); return <div className="scriptingObject-leaf">{title}</div>; } else { const name = String(val); - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); return <div className="scriptingObject-leaf">{title}</div>; } } @@ -65,11 +94,11 @@ export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () @observer export class ScriptingRepl extends React.Component { - @observable private commands: { command: string, result: any }[] = []; + @observable private commands: { command: string; result: any }[] = []; private commandsHistory: string[] = []; - @observable private commandString: string = ""; - private commandBuffer: string = ""; + @observable private commandString: string = ''; + private commandBuffer: string = ''; @observable private historyIndex: number = -1; @@ -82,7 +111,7 @@ export class ScriptingRepl extends React.Component { transformer: context => { const knownVars: { [name: string]: number } = {}; const usedDocuments: number[] = []; - ScriptingGlobals.getGlobals().forEach((global: any) => knownVars[global] = 1); + ScriptingGlobals.getGlobals().forEach((global: any) => (knownVars[global] = 1)); return root => { function visit(node: ts.Node) { let skip = false; @@ -105,7 +134,7 @@ export class ScriptingRepl extends React.Component { const m = parseInt(match[1]); usedDocuments.push(m); } else { - return ts.createPropertyAccess(ts.createIdentifier("args"), node); + return ts.createPropertyAccess(ts.createIdentifier('args'), node); } } } @@ -114,20 +143,20 @@ export class ScriptingRepl extends React.Component { } return ts.visitNode(root, visit); }; - } + }, }; - } + }; @action onKeyDown = (e: React.KeyboardEvent) => { let stopProp = true; switch (e.key) { - case "Enter": { + case 'Enter': { e.stopPropagation(); const docGlobals: { [name: string]: any } = {}; - Array.from(DocumentManager.Instance.DocumentViews).forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document); + DocumentManager.Instance.DocumentViews.forEach((dv, i) => (docGlobals[`d${i}`] = dv.props.Document)); const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals); - const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals }); + const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals }); if (!script.compiled) { this.commands.push({ command: this.commandString, result: script.errors }); return; @@ -139,13 +168,13 @@ export class ScriptingRepl extends React.Component { this.maybeScrollToBottom(); - this.commandString = ""; - this.commandBuffer = ""; + this.commandString = ''; + this.commandBuffer = ''; this.historyIndex = -1; } break; } - case "ArrowUp": { + case 'ArrowUp': { if (this.historyIndex < this.commands.length - 1) { this.historyIndex++; if (this.historyIndex === 0) { @@ -155,12 +184,12 @@ export class ScriptingRepl extends React.Component { } break; } - case "ArrowDown": { + case 'ArrowDown': { if (this.historyIndex >= 0) { this.historyIndex--; if (this.historyIndex === -1) { this.commandString = this.commandBuffer; - this.commandBuffer = ""; + this.commandBuffer = ''; } else { this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex]; } @@ -176,25 +205,25 @@ export class ScriptingRepl extends React.Component { e.stopPropagation(); e.preventDefault(); } - } + }; @action onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.commandString = e.target.value; - } + }; private shouldScroll: boolean = false; private maybeScrollToBottom = () => { const ele = this.commandsRef.current; - if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) { + if (ele && ele.scrollTop === ele.scrollHeight - ele.offsetHeight) { this.shouldScroll = true; this.forceUpdate(); } - } + }; private scrollToBottom() { const ele = this.commandsRef.current; - ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight }); + ele && ele.scroll({ behavior: 'auto', top: ele.scrollHeight }); } componentDidUpdate() { @@ -206,15 +235,11 @@ export class ScriptingRepl extends React.Component { overlayDisposer?: () => void; onFocus = () => { - if (this.overlayDisposer) { - this.overlayDisposer(); - } + this.overlayDisposer?.(); this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; - onBlur = () => { - this.overlayDisposer?.(); - } + onBlur = () => this.overlayDisposer?.(); render() { return ( @@ -229,14 +254,8 @@ export class ScriptingRepl extends React.Component { ); })} </div> - <input - className="scriptingRepl-commandInput" - onFocus={this.onFocus} - onBlur={this.onBlur} - value={this.commandString} - onChange={this.onChange} - onKeyDown={this.onKeyDown}></input> + <input className="scriptingRepl-commandInput" onFocus={this.onFocus} onBlur={this.onBlur} value={this.commandString} onChange={this.onChange} onKeyDown={this.onKeyDown}></input> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index f3397e2c4..99a68e94b 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -372,7 +372,8 @@ .editableView-container-editing-oneLine, .editableView-container-editing { color: grey; - padding-top: 10px; + padding-top: 5px; + padding-bottom: 5px; width: 100%; } @@ -387,7 +388,8 @@ letter-spacing: 2px; color: grey; border: 0px; - padding-top: 10px; // 12px 10px 11px 10px; + padding-top: 5px; // 12px 10px 11px 10px; + padding-bottom: 5px; // 12px 10px 11px 10px; } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 29bdc0e2d..08e3174d1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -53,7 +53,6 @@ import { MarqueeView } from './MarqueeView'; import React = require('react'); export type collectionFreeformViewProps = { - noPointerWheel?: () => boolean; // turn off pointerwheel interactions (see PDFViewer) NativeWidth?: () => number; NativeHeight?: () => number; originTopLeft?: boolean; @@ -1023,13 +1022,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection deltaScale = 20 / invTransform.Scale; } if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) { + this.setPan(0, 0); return; } if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) { deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale; } - const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + const localTransform = invTransform.scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); @@ -1039,17 +1039,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.props.noPointerWheel?.() || this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom + if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom PresBox.Instance?.pauseAutoPres(); if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); - e.preventDefault(); switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if ctrl is selected then zoom if (e.ctrlKey) { if (this.props.isContentActive(true)) { - this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + if (this.props.isAnnotationOverlayScrollable) { + // bcz: zooming on a webbox doesn't get the correct coordinates here for unknown reasons. + // so better to do nothing than having things jump around. + } else this.zoom(e.screenX, e.screenY, e.deltaY); } } // otherwise pan else if (this.props.isContentActive(true)) { @@ -1123,16 +1125,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) + (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); - if (NumCast(this.rootDoc.scrollTop) && NumCast(this.rootDoc._viewScale, minScale) !== minScale) { + if (false && NumCast(this.rootDoc.scrollTop) && NumCast(this.rootDoc._viewScale, minScale) !== minScale) { const relTop = NumCast(this.rootDoc.scrollTop) / maxScrollTop; this.rootDoc.scrollTop = undefined; newPanY = minPanY + relTop * (maxPanY - minPanY); } else if (fitYscroll && this.rootDoc.scrollTop === undefined && NumCast(this.rootDoc._viewScale, minScale) === minScale) { const maxPanY = minPanY + fitYscroll; const relTop = (panY - minPanY) / (maxPanY - minPanY); - setTimeout(() => { - this.rootDoc.scrollTop = relTop * maxScrollTop; - }, 10); + setTimeout(() => (this.rootDoc.scrollTop = relTop * maxScrollTop), 10); newPanY = minPanY; } !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index a9434fde3..3a0c2c85c 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -182,8 +182,10 @@ .schema-table-cell, .row-menu { border: 1px solid $medium-gray; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; padding: 5px; + display: inline-block; } .schema-row { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index a59d7e5a3..588affc1c 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -56,7 +56,8 @@ export class CollectionSchemaView extends CollectionSubView() { private _documentOptions: DocumentOptions = new DocumentOptions(); private _tableContentRef: HTMLDivElement | null = null; - public static _rowHeight: number = 50; + static _rowHeight: number = 50; + static _rowSingleLineHeight: number = 32; public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; @@ -515,9 +516,9 @@ export class CollectionSchemaView extends CollectionSubView() { if (found) { const rect = found.getBoundingClientRect(); const localRect = this.props.ScreenToLocalTransform().transformBounds(rect.left, rect.top, rect.width, rect.height); - if (localRect.y < CollectionSchemaView._rowHeight || localRect.y + localRect.height > this.props.PanelHeight()) { + if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this.props.PanelHeight()) { let focusSpeed = options.zoomTime ?? 50; - smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - CollectionSchemaView._rowHeight, options.easeFunc); + smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc); return focusSpeed; } } @@ -836,6 +837,7 @@ export class CollectionSchemaView extends CollectionSubView() { }); return { docs }; } + rowHeightFunc = () => (BoolCast(this.layoutDoc._singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight); sortedDocsFunc = () => this.sortedDocs; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0); @@ -850,7 +852,7 @@ export class CollectionSchemaView extends CollectionSubView() { // prevent wheel events from passively propagating up through containers r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false }); }}> - <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}> + <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}> <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}> <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}> <FontAwesomeIcon icon="plus" /> @@ -865,6 +867,7 @@ export class CollectionSchemaView extends CollectionSubView() { sortField={this.sortField} sortDesc={this.sortDesc} setSort={this.setSort} + rowHeight={this.rowHeightFunc} removeColumn={this.removeColumn} resizeColumn={this.startResize} openContextMenu={this.openContextMenu} @@ -875,7 +878,7 @@ export class CollectionSchemaView extends CollectionSubView() { </div> {this._columnMenuIndex !== undefined && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} - <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> + <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} /> </div> {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} @@ -922,20 +925,20 @@ interface CollectionSchemaViewDocsProps { schema: CollectionSchemaView; setRef: (ref: HTMLDivElement | null) => void; childDocs: () => { docs: Doc[] }; + rowHeight: () => number; } @observer class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> { tableWidthFunc = () => this.props.schema.tableWidth; - rowHeightFunc = () => CollectionSchemaView._rowHeight; - childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -CollectionSchemaView._rowHeight - index * this.rowHeightFunc())); + childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -this.props.rowHeight() - index * this.props.rowHeight())); render() { return ( - <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + CollectionSchemaView._rowHeight}px)` }}> + <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}> {this.props.childDocs().docs.map((doc: Doc, index: number) => { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc; return ( - <div className="schema-row-wrapper" style={{ height: CollectionSchemaView._rowHeight }}> + <div className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}> <DocumentView key={doc[Id]} {...this.props.schema.props} @@ -945,7 +948,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP DataDoc={dataDoc} renderDepth={this.props.schema.props.renderDepth + 1} PanelWidth={this.tableWidthFunc} - PanelHeight={this.rowHeightFunc} + PanelHeight={this.props.rowHeight} styleProvider={DefaultStyleProvider} waitForDoubleClickToClick={returnNever} defaultDoubleClick={returnIgnore} diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 243fe0c61..7da3c042c 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -16,6 +16,7 @@ export interface SchemaColumnHeaderProps { sortDesc: boolean; setSort: (field: string | undefined, desc?: boolean) => void; removeColumn: (index: number) => void; + rowHeight: () => number; resizeColumn: (e: any, index: number) => void; dragColumn: (e: any, index: number) => boolean; openContextMenu: (x: number, y: number, index: number) => void; diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index ca9e0bda0..34631e0b7 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -2,6 +2,9 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; +import { Doc } from '../../../../fields/Doc'; +import { BoolCast } from '../../../../fields/Types'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch } from '../../../util/UndoManager'; @@ -12,8 +15,6 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { CollectionSchemaView } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaTableCell } from './SchemaTableCell'; -import { computedFn } from 'mobx-utils'; -import { Doc } from '../../../../fields/Doc'; @observer export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -95,7 +96,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="schema-row" - style={{ height: CollectionSchemaView._rowHeight, backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined }} + style={{ height: this.props.PanelHeight(), backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} ref={(row: HTMLDivElement | null) => { @@ -132,13 +133,16 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { Document={this.rootDoc} col={index} fieldKey={key} + allowCRs={false} // to enter text with new lines, must use \n columnWidth={this.columnWidth(index)} + rowHeight={this.schemaView.rowHeightFunc} isRowActive={this.props.isContentActive} getFinfo={this.getFinfo} selectCell={this.selectCell} deselectCell={this.deselectCell} selectedCell={this.selectedCell} setColumnValues={this.setColumnValues} + oneLine={BoolCast(this.schemaDoc?._singleLine)} /> ))} </div> diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 712bd4491..12daa1252 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -5,22 +5,24 @@ import { extname } from 'path'; import DatePicker from 'react-datepicker'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; +import { RichTextField } from '../../../../fields/RichTextField'; import { BoolCast, Cast, DateCast, FieldValue } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils'; import { FInfo } from '../../../documents/Documents'; +import { DocFocusOrOpen } from '../../../util/DocumentManager'; import { dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; +import { OpenWhere } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; -import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView'; +import { ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; -import { RichTextField } from '../../../../fields/RichTextField'; -import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; export interface SchemaTableCellProps { Document: Doc; @@ -29,17 +31,24 @@ export interface SchemaTableCellProps { selectCell: (doc: Doc, col: number) => void; selectedCell: () => [Doc, number] | undefined; fieldKey: string; + maxWidth?: () => number; columnWidth: () => number; + rowHeight: () => number; + padding?: number; // default is 5 -- see scss isRowActive: () => boolean | undefined; getFinfo: (fieldKey: string) => FInfo | undefined; setColumnValues: (field: string, value: string) => boolean; + oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs + allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit) + finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView) } @observer export class SchemaTableCell extends React.Component<SchemaTableCellProps> { - public static colRowHeightFunc() { - return CollectionSchemaView._rowHeight; - } + static addFieldDoc = (doc: Doc, where: OpenWhere) => { + DocFocusOrOpen(doc); + return true; + }; public static renderProps(props: SchemaTableCellProps) { const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props; let protoCount = 0; @@ -71,12 +80,12 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, - addDocTab: returnFalse, + addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, Document, fieldKey, PanelWidth: columnWidth, - PanelHeight: SchemaTableCell.colRowHeightFunc, + PanelHeight: props.rowHeight, }; const readOnly = getFinfo(fieldKey)?.readOnly ?? false; const cursor = !readOnly ? 'text' : 'default'; @@ -98,8 +107,11 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { style={{ color, textDecoration, + width: '100%', }}> <EditableView + oneLine={this.props.oneLine} + allowCRs={this.props.allowCRs} contents={<FieldView {...fieldProps} />} editing={this.selected ? undefined : false} GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} @@ -107,7 +119,9 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { if (shiftDown && enterKey) { this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); } - return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return ret; })} /> </div> @@ -121,7 +135,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; if (typeof cellValue === 'string') return ColumnType.Any; - if (typeof cellValue === 'boolean') return ColumnType.Any; + if (typeof cellValue === 'boolean') return ColumnType.Boolean; const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; if (columnTypeStr && columnTypeStr in FInfotoColType) { @@ -148,7 +162,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { <div className="schema-table-cell" onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))} - style={{ width: this.props.columnWidth(), border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> + style={{ padding: this.props.padding, maxWidth: this.props.maxWidth?.(), width: this.props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> {this.content} </div> ); @@ -211,8 +225,8 @@ export class SchemaImageCell extends React.Component<SchemaTableCellProps> { const aspect = Doc.NativeAspect(this.props.Document); // aspect ratio // let width = Math.max(75, this.props.columnWidth); // get a with that is no smaller than 75px // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px - const height = CollectionSchemaView._rowHeight - 10; - const width = height * aspect; // increase the width of the image if necessary to maintain proportionality + const height = this.props.rowHeight() ? this.props.rowHeight() - (this.props.padding || 6) * 2 : undefined; + const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality return <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />; } @@ -288,7 +302,9 @@ export class SchemaBoolCell extends React.Component<SchemaTableCellProps> { if (shiftDown && enterKey) { this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); } - return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + const set = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return set; })} /> </div> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a8c8a0d2e..862d0da8c 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -12,12 +12,11 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; +import { InkingStroke } from '../InkingStroke'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); -import { DocumentType } from '../../documents/DocumentTypes'; -import { InkingStroke } from '../InkingStroke'; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 56de2d1fc..6e2ed72b8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -1,23 +1,39 @@ - -import { observer } from "mobx-react"; -import * as React from "react"; -import { DocumentView } from "./DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Transformer, ts } from "../../util/Scripting"; -import { Field } from "../../../fields/Doc"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { DocumentView } from './DocumentView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { Transformer, ts } from '../../util/Scripting'; +import { Field } from '../../../fields/Doc'; +import { Tooltip } from '@material-ui/core'; +import { action, observable } from 'mobx'; +import { Id } from '../../../fields/FieldSymbols'; +import { factory } from 'typescript'; +import { LightboxView } from '../LightboxView'; @observer -export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { +export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { + @observable _hovered = false; + static get DocViews() { + return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews; + } render() { const view = this.props.view; const { left, top, right, bottom } = view.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 }; return ( - <div className="documentIcon-outerDiv" style={{ - position: "absolute", - transform: `translate(${(left + right) / 2}px, ${top}px)`, - }}> - <p>d{this.props.index}</p> + <div + className="documentIcon-outerDiv" + onPointerEnter={action(e => (this._hovered = true))} + onPointerLeave={action(e => (this._hovered = false))} + style={{ + pointerEvents: 'all', + opacity: this._hovered ? 0.3 : 1, + position: 'absolute', + transform: `translate(${(left + right) / 2}px, ${top}px)`, + }}> + <Tooltip title={<>{this.props.view.rootDoc.title}</>}> + <p>d{this.props.index}</p> + </Tooltip> </div> ); } @@ -41,7 +57,9 @@ export class DocumentIconContainer extends React.Component { const match = node.text.match(/d([0-9]+)/); if (match) { const m = parseInt(match[1]); + const doc = DocumentIcon.DocViews[m].rootDoc; usedDocuments.add(m); + return factory.createIdentifier(`idToDoc("${doc[Id]}")`); } } } @@ -52,14 +70,14 @@ export class DocumentIconContainer extends React.Component { }; }, getVars() { - const docs = Array.from(DocumentManager.Instance.DocumentViews); + const docs = DocumentIcon.DocViews; const capturedVariables: { [name: string]: Field } = {}; - usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document); - return { capturedVariables }; - } + usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`)); + return capturedVariables; + }, }; } render() { - return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); + return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b61a468bd..ffbde18ca 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -48,6 +48,7 @@ import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); import { InkingStroke } from '../InkingStroke'; +import { RefField } from '../../../fields/RefField'; const { Howl } = require('howler'); interface Window { @@ -127,6 +128,7 @@ export interface DocComponentView { IsPlaying?: () => boolean; // is a media document playing TogglePause?: (keep?: boolean) => void; // toggle media document playing state setFocus?: () => void; // sets input focus to the componentView + setData?: (data: Field | Promise<RefField | undefined>) => boolean; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) @@ -434,7 +436,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) { - UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else { diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index e317de11e..f3c8ba62d 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,10 @@ import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { DocumentIconContainer } from './DocumentIcon'; export type KVPScript = { script: CompiledScript; @@ -75,7 +79,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { value = dubEq ? value.substring(2) : value; const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true }; if (dubEq) options.typecheck = false; - const script = CompileScript(value, options); + const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() }); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 5b660e582..57d36932e 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,60 +1,52 @@ -@import "../global/globalCssVariables"; - +@import '../global/globalCssVariables'; .keyValuePair-td-key { - display:inline-block; + display: inline-block; - .keyValuePair-td-key-container{ - width:100%; - height:100%; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - .keyValuePair-td-key-delete{ + .keyValuePair-td-key-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + .keyValuePair-td-key-delete { position: relative; background-color: transparent; - color:red; + color: red; } .keyValuePair-td-key-check { position: relative; margin: 0; } .keyValuePair-keyField { - width:100%; + width: 100%; margin-left: 20px; - margin-top: -1px; font-family: monospace; - // text-align: center; - align-self: center; position: relative; overflow: auto; + display: inline; } } } .keyValuePair-td-value { - display:inline-block; + display: inline-block; overflow: scroll; font-family: monospace; height: 30px; - .keyValuePair-td-value-container { - display: flex; - align-items: center; - align-content: center; - flex-direction: row; - justify-content: space-between; - flex-wrap: nowrap; - width: 100%; - height: 100%; + .keyValuePair-td-value-container { + display: inline; + justify-content: space-between; + width: 100%; + height: 100%; - img { - max-height: 36px; - width: auto; - } - .videoBox-cont{ - width: auto; - max-height: 36px; - } + img { + max-height: 36px; + width: auto; + } + .videoBox-cont { + width: auto; + max-height: 36px; + } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 7ea6d42ff..85641124f 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -118,13 +118,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> <div className="keyValuePair-td-value-container"> - <EditableView - contents={contents} - maxHeight={36} - height={'auto'} - GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} - SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} - /> + <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> </div> </td> </tr> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e05b48c0b..82c8e796d 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,16 +2,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, removeStyleSheetRule, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -31,12 +31,13 @@ import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; +import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; import './WebBox.scss'; import React = require('react'); +import { RefField } from '../../../fields/RefField'; const { CreateImage } = require('./WebBoxRenderer'); const _global = (window /* browser */ || global) /* node */ as any; const htmlToText = require('html-to-text'); @@ -47,6 +48,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; + static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -58,15 +60,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ''; + private _scrollTimer: any; private get _getAnchor() { return AnchorMenu.Instance?.GetAnchor; } - @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 _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 want 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 _webPageHasBeenRendered = false; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -141,51 +143,40 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; updateThumb = async () => { - const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href; + if (!this._iframe) return; const scrollTop = NumCast(this.layoutDoc._scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); - if ( - !this.props.isSelected(true) && - !Doc.IsBrushedDegree(this.rootDoc) && - !this.isAnyChildContentActive() && - !this.rootDoc.thumbLockout && - !this.props.dontRegisterView && - this._iframe && - !imageBitmap && - (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight) - ) { - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); - if (!htmlString) { - htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); - } - this.layoutDoc.thumb = undefined; - this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. - CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) - .then((data_url: any) => { - if (data_url.includes('<!DOCTYPE')) { - console.log('BAD DATA IN THUMB CREATION'); - return; - } - Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => - setTimeout( - action(() => { - this.rootDoc.thumbLockout = false; - this.layoutDoc.thumb = new ImageField(returnedfilename); - this.layoutDoc.thumbScrollTop = scrollTop; - this.layoutDoc.thumbNativeWidth = nativeWidth; - this.layoutDoc.thumbNativeHeight = nativeHeight; - }), - 500 - ) - ); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); + var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + if (!htmlString) { + htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } + this.layoutDoc.thumb = undefined; + this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. + CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) + .then((data_url: any) => { + if (data_url.includes('<!DOCTYPE')) { + console.log('BAD DATA IN THUMB CREATION'); + return; + } + Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + setTimeout( + action(() => { + this.rootDoc.thumbLockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }), + 500 + ) + ); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); }; - _thumbTimer: any; + async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) @@ -196,24 +187,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"])`; reqdFuncs[this.fieldKey + '-annotations-setter'] = `this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"] = value`; reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"])`; - DocUtils.AssignScripts(this.rootDoc, {}, reqdFuncs); + DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); }); - reaction( - () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), - async selected => { - if (selected) { - this._thumbTimer && clearTimeout(this._thumbTimer); - this._webPageHasBeenRendered = true; - } else if ( - (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) - LightboxView.LightboxDoc !== this.rootDoc - ) { - // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - this._thumbTimer && clearTimeout(this._thumbTimer); - this._thumbTimer = setTimeout(this.updateThumb, 2000); - } - }, - { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } + this._disposers.urlchange = reaction( + () => WebCast(this.rootDoc.data), + url => { + this.submitURL(url.url.href, false, false); + } ); this._disposers.autoHeight = reaction( @@ -296,8 +276,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._savedAnnotations; }; - menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); focus = (anchor: Doc, options: DocFocusOptions) => { @@ -316,9 +294,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; - getView = async (doc: Doc) => { + @action + getView = (doc: Doc) => { if (this.rootDoc.layoutKey === 'layout_icon') this.props.DocumentView?.().iconify(); - if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl)); + if (this._url && WebCast(doc.presData).url.href !== this._url) this.setData(WebCast(doc.presData).url.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; @@ -341,13 +320,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations, false) ?? - Docs.Create.WebanchorDocument(this._url, { + Docs.Create.WebanchorDocument({ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop), y: NumCast(this.layoutDoc._scrollTop), unrendered: true, annotationOn: this.rootDoc, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.textHtml = ele?.innerHTML; //addAsAnnotation && @@ -413,7 +392,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; - addStyleSheet(document: any, styleType: string = 'text/css') { + addWebStyleSheet(document: any, styleType: string = 'text/css') { if (document) { const style = document.createElement('style'); style.type = styleType; @@ -421,7 +400,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return (sheets as any).sheet; } } - addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { + addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = typeof css === 'string' ? css @@ -438,8 +417,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); } - - this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); + this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0; + this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); let href: Opt<string>; try { @@ -463,12 +442,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .replace('search&', 'search?') .replace('?gbv=1', ''); } - this.submitURL(requrlraw, undefined, true); + this.setData(requrlraw); } const iframeContent = iframe?.contentDocument; if (iframeContent) { iframeContent.addEventListener('pointerup', this.iframeUp); iframeContent.addEventListener('pointerdown', this.iframeDown); + // iframeContent.addEventListener( + // 'wheel', + // e => { + // e.ctrlKey && e.preventDefault(); + // }, + // { passive: false } + // ); const initHeights = () => { this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); if (this._scrollHeight) { @@ -482,11 +468,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps action(() => initHeights), 5000 ); - iframe.setAttribute('enable-annotation', 'true'); iframeContent.addEventListener( 'click', undoBatch( action((e: MouseEvent) => { + const batch = UndoManager.StartBatch('webclick'); let href = ''; 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; @@ -494,7 +480,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const origin = this.webField?.origin; if (href && origin) { e.stopPropagation(); - setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin))); + setTimeout(() => { + this.setData(href.replace(Utils.prepend(''), origin)); + batch.end(); + }); if (this._outerRef.current) { this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); this._outerRef.current.scrollLeft = 0; @@ -503,39 +492,49 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }) ) ); - iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); - //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); + iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false }); } }; @action - iframeWheel = (e: any) => { + iframeWheel = (e: WheelEvent) => { if (!this._scrollTimer) { - this._scrollTimer = setTimeout( - action(() => (this._scrollTimer = undefined)), - 250 - ); // this turns events off on the iframe which allows scrolling to change direction smoothly + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + this._scrollTimer = undefined; + clearStyleSheetRules(WebBox.webStyleSheet); + }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + } + if (e.ctrlKey) { + if (this._innerCollectionView) { + this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY); + const offset = e.clientY - NumCast(this.layoutDoc._scrollTop); + this.layoutDoc.panY = offset - offset / NumCast(this.layoutDoc._viewScale) + NumCast(this.layoutDoc._scrollTop) - NumCast(this.layoutDoc._scrollTop) / NumCast(this.layoutDoc._viewScale); + } + e.preventDefault(); } }; @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); - this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout( - action(() => { - this._scrollTimer = undefined; - const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc.thumb = undefined; - this.layoutDoc.thumbScrollTop = undefined; - this.layoutDoc.thumbNativeWidth = undefined; - this.layoutDoc.thumbNativeHeight = undefined; - this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; - } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; - }), - timeout - ); + if (this._scrollTimer) { + clearTimeout(this._scrollTimer); + clearStyleSheetRules(WebBox.webStyleSheet); + } + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + clearStyleSheetRules(WebBox.webStyleSheet); + this._scrollTimer = undefined; + const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; + } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; + }, timeout); }; goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { @@ -557,8 +556,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { if (future.length) { const curUrl = this._url; - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); - this.rootDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -578,9 +578,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.rootDoc[this.fieldKey + '-future'] = new List<string>([this._url]); - else this.rootDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]); - this.layoutDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + 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()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -607,23 +608,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!newUrl) return; if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; try { - const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string')); - const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string')); - const url = this.webField?.toString(); - if (url && !preview) { - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]); - this.layoutDoc._scrollTop = 0; + if (!preview) { if (this._webPageHasBeenRendered) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; this.layoutDoc.thumbNativeHeight = undefined; } - future && (future.length = 0); } if (!preview) { - this.layoutDoc[this.fieldKey] = new WebField(new URL(newUrl)); - !dontUpdateIframe && (this._webUrl = this._url); + if (!dontUpdateIframe) { + this._webUrl = this._url; + } } } catch (e) { console.log('WebBox URL error:' + this._url); @@ -637,48 +633,28 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const uri = dataTransfer.getData('text/uri-list'); const url = uri || html || this._url || ''; const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; - this.submitURL(newurl); + this.setData(newurl); e.stopPropagation(); }; + + @action + setData = (data: Field | Promise<RefField | undefined>) => { + if (!(typeof data === 'string') && !(data instanceof WebField)) return false; + if (Field.toString(data) === this._url) return false; + this._scrollHeight = 0; + const oldUrl = this._url; + const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []); + const weburl = new WebField(Field.toString(data)); + this.dataDoc[this.fieldKey + '-future'] = new List<string>([]); + this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), oldUrl]); + this.dataDoc[this.fieldKey] = weburl; + return true; + }; onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { - e.key === 'Enter' && this.submitURL(this._keyInput.current!.value); + if (e.key === 'Enter') this.setData(this._keyInput.current!.value); e.stopPropagation(); }; - @computed get urlEditor() { - return ( - <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}> - <input - className="collectionMenu-urlInput" - key={this._url} - placeholder="ENTER URL" - defaultValue={this._url} - onDrop={this.onWebUrlDrop} - onDragOver={e => e.preventDefault()} - onKeyDown={this.onWebUrlValueKeyDown} - onClick={e => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}> - <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={() => this.back}> - {' '} - <FontAwesomeIcon icon="caret-left" size="lg" />{' '} - </button> - <button className="submitUrl" onClick={() => this.forward}> - {' '} - <FontAwesomeIcon icon="caret-right" size="lg" />{' '} - </button> - </div> - </div> - ); - } - specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; @@ -706,6 +682,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, icon: 'snowflake', }); + funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' }); cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -746,19 +723,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @computed get urlContent() { - if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null; - this.props.thumbShown?.(); + setTimeout( + action(() => { + if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { + this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); + } + this._webPageHasBeenRendered = true; + }) + ); const field = this.rootDoc[this.props.fieldKey]; - let view; if (field instanceof HtmlField) { - view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; - } else if (field instanceof WebField) { + return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; + } + if (field instanceof WebField) { const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; - view = ( + return ( <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} @@ -767,32 +748,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps 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 - ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} - src={'https://crossorigin.me/https://cs.brown.edu'} - /> - ); } - setTimeout( - action(() => { - this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); - if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { - this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); - } - this._webPageHasBeenRendered = true; - }) - ); - return view; + return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />; } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - console.log(annotationKey); - (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url)); + (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url))); return this.addDocument(doc, annotationKey); }; @@ -843,7 +804,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> @@ -878,17 +839,33 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } } }); + @action + onZoomWheel = (e: React.WheelEvent) => { + if (this.props.isContentActive(true)) { + e.stopPropagation(); + } + }; sidebarWidth = () => { if (!this.SidebarShown) return 0; if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; + _innerCollectionView: CollectionFreeFormView | undefined; + zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; + setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView); + @computed get content() { const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; return ( - <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}> - {this.urlContent} + <div + className={'webBox-cont' + (interactive ? '-interactive' : '')} + onKeyDown={e => e.stopPropagation()} + style={{ + width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%', + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.panX)}px, ${-NumCast(this.layoutDoc.panY)}px)`, + }}> + {this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc) ? null : this.urlContent} </div> ); } @@ -896,7 +873,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get annotationLayer() { TraceMobx(); return ( - <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + <div + className="webBox-annotationLayer" + style={{ + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.panX)}px, ${-NumCast(this.layoutDoc.panY)}px)`, + height: Doc.NativeHeight(this.Document) || undefined, + }} + ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( @@ -916,9 +899,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const renderAnnotations = (docFilters: () => string[]) => ( <CollectionFreeFormView {...this.props} - setContentView={emptyFunction} + setContentView={this.setInnerContent} NativeWidth={returnZero} NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.annotationKey} @@ -945,16 +930,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); return ( <div - className={'webBox-outerContent'} + className="webBox-outerContent" ref={this._outerRef} style={{ height: `${100 / scale}%`, pointerEvents, }} - onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe + // when active, block wheel events from propagating since they're handled by the iframe + onWheel={this.onZoomWheel} onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> - <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}> + <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> {this.content} {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} {renderAnnotations(this.opaqueFilter)} @@ -1016,7 +1002,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); render() { - setTimeout(() => DocListCast(this.rootDoc[this.annotationKey]).forEach(doc => (doc.webUrl = this._url))); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 26515da30..a4968dcd0 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -505,7 +505,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className="menuButton editableText"> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> </div> </div> ); @@ -889,7 +889,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) { if (checkResult) { return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); } - (selected.ComponentView as WebBox).submitURL(url); + selected.ComponentView?.setData?.(url); //selected.rootDoc.data = new WebField(url); } }); @@ -925,6 +925,15 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) { } } }); +ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + return NumCast(selected._singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (selected) { + selected._singleLine = !selected._singleLine; + } +}); /** STACK * groupBy diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index bf6fa2ec6..a9e59ab1c 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -2,18 +2,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; +import { DataSym, Doc, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { ComputedField } from '../../../../fields/ScriptField'; import { Cast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; @@ -70,10 +69,10 @@ export class DashFieldView { } catch {} }); } - deselectNode() { + @action deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } - selectNode() { + @action selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } } @@ -98,6 +97,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); @observable _dashDoc: Doc | undefined; + @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); @@ -114,126 +114,32 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._reactionDisposer?.(); } - public static multiValueDelimeter = ';'; - public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) { - const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? ''; - const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal; - return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' }; - } - // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { - if (this._dashDoc) { - const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey); - // field value is a boolean, so use a checkbox or similar widget to display it - if (boolVal === true || boolVal === false) { - return ( - <input - className="dashFieldView-fieldCheck" - type="checkbox" - checked={boolVal} - onChange={e => { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true); - }} - /> - ); - } // field value is a string, so display it as an editable span - else { - // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't - // use React events. Essentially, React events occur after native events have been processed, so corresponding React events - // will never fire because Prosemirror has handled the native events. So we add listeners for native events here. - return ( - <span - className="dashFieldView-fieldSpan" - contentEditable={!this.props.unclickable()} - style={{ display: strVal.length < 2 ? 'inline-block' : undefined }} - suppressContentEditableWarning={true} - defaultValue={strVal} - ref={r => { - r?.addEventListener('keydown', e => this.fieldSpanKeyDown(e, r)); - r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false)); - r?.addEventListener( - 'pointerdown', - action(e => { - // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - // while (target && !target.dataset?.targethrefs) target = target.parentElement; - this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos())))); - // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); - // e.stopPropagation(); - }) - ); - }}> - {strVal} - </span> - ); - } - } + return !this._dashDoc ? null : ( + <div onClick={action(e => (this._expanded = !this.props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}> + <SchemaTableCell + Document={this._dashDoc} + col={0} + deselectCell={emptyFunction} + selectCell={emptyFunction} + maxWidth={this.props.hideKey ? undefined : () => 100} + columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + selectedCell={() => [this._dashDoc!, 0]} + fieldKey={this._fieldKey} + rowHeight={returnZero} + isRowActive={() => this._expanded && this.props.editable} + padding={0} + getFinfo={emptyFunction} + setColumnValues={returnFalse} + allowCRs={true} + oneLine={!this._expanded} + finishEdit={action(() => (this._expanded = false))} + /> + </div> + ); } - // we need to handle all key events on the input span or else they will propagate to prosemirror. - @action - fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { - if (e.key === 'c' && (e.ctrlKey || e.metaKey)) { - navigator.clipboard.writeText(window.getSelection()?.toString() || ''); - return; - } - if (e.key === 'Enter') { - // handle the enter key by "submitting" the current text to Dash's database. - this.updateText(span.textContent!, true); - e.preventDefault(); // prevent default to avoid a newline from being generated and wiping out this field view - } - if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { - // handle ctrl-A to select all the text within the span - if (window.getSelection) { - const range = document.createRange(); - range.selectNodeContents(span); - window.getSelection()!.removeAllRanges(); - window.getSelection()!.addRange(range); - } - e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected - } - if (!this.props.editable) { - e.preventDefault(); - } - e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror. - }; - - @action - updateText = (nodeText: string, forceMatch: boolean) => { - if (nodeText) { - const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText; - // look for a document whose id === the fieldKey being displayed. If there's a match, then that document - // holds the different enumerated values for the field in the titles of its collected documents. - // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. - DocServer.GetRefField(this._fieldKey).then(options => { - let modText = ''; - options instanceof Doc && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title))); - if (modText) { - // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); - } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key - else if (nodeText.startsWith(':=')) { - this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); - } else if (nodeText.startsWith('=:=')) { - Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); - } else { - if (Number(newText).toString() === newText) { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); - Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true); - } else { - const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); - if (!this._textBoxDoc[this._fieldKey]) { - const strVal = splits.length > 1 ? new List<string>(splits) : newText; - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true); - } - } - } - }); - } - }; - createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); if (container) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index b5aa34a29..68160ac9b 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1575,7 +1575,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - !this.props.isSelected(true) && editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + // !this.props.isSelected(true) && + editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 5b47e8a70..7e17008bb 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -46,7 +46,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; + return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index bd2be8f11..c22107c03 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio import { observer } from 'mobx-react'; import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; -import { InkField, InkTool } from '../../../../fields/InkField'; +import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; @@ -429,6 +429,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } } + if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { + bestTarget._dataTransition = `all ${transTime}ms`; + const fkey = Doc.LayoutFieldKey(bestTarget); + const setData = bestTargetView?.ComponentView?.setData; + if (setData) setData(activeItem.presData); + else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + bestTarget[fkey + '-usePath'] = activeItem.presUsePath; + setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) { if (bestTarget.xRange !== activeItem.presXRange) { bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy](); @@ -521,13 +530,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); Doc.GetProto(bestTarget)[fkey + '-annotations'] = newList; } - if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { - bestTarget._dataTransition = `all ${transTime}ms`; - const fkey = Doc.LayoutFieldKey(bestTarget); - Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - bestTarget[fkey + '-usePath'] = activeItem.presUsePath; - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); - } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) { changed = true; const layoutField = Doc.LayoutFieldKey(bestTarget); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index fce67e7fc..ea709a859 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -528,7 +528,6 @@ export class PDFViewer extends React.Component<IViewerProps> { style={{ mixBlendMode: mixBlendMode, display: display, - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined, }}> <CollectionFreeFormView @@ -537,7 +536,7 @@ export class PDFViewer extends React.Component<IViewerProps> { NativeHeight={returnZero} setContentView={emptyFunction} // override setContentView to do nothing pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. - childPointerEvents={'all'} // but freeform children need to get events to allow text editing, etc + childPointerEvents="all" // but freeform children need to get events to allow text editing, etc renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.props.fieldKey + '-annotations'} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index b8ac8fb5d..5e3e27a7c 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -41,7 +41,7 @@ export namespace Field { switch (typeof field) { case 'string': if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s - return `"${field}"`; + return !field.includes('`') ? `\`${field}\`` : `"${field}"`; case 'number': case 'boolean': return String(field); |