diff options
author | A.J. Shulman <Shulman.aj@gmail.com> | 2024-07-17 14:59:45 -0400 |
---|---|---|
committer | A.J. Shulman <Shulman.aj@gmail.com> | 2024-07-17 14:59:45 -0400 |
commit | b7b3604f54ff5174b7f9a2f115362ea11d1a83ae (patch) | |
tree | 2378a214b6f624451228903808d06c9337142feb /src | |
parent | dd379c9e4c4c214b2ed14e3fd9a5f966e4f03b48 (diff) | |
parent | 9da1206079b2f20274a720c4e62cf1d6e063e7ac (diff) |
Merge branch 'master' into aj-system-message
Diffstat (limited to 'src')
30 files changed, 400 insertions, 320 deletions
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index d03ae1486..630d7edbc 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -86,6 +86,7 @@ export function returnEmptyDoclist() { return [] as any[]; } +// eslint-disable-next-line @typescript-eslint/no-namespace export namespace ClientUtils { export const CLICK_TIME = 300; export const DRAG_THRESHOLD = 4; @@ -449,30 +450,29 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement | animateScroll(); } -export function addStyleSheet(styleType: string = 'text/css') { +export function addStyleSheet() { const style = document.createElement('style'); - style.type = styleType; const sheets = document.head.appendChild(style); - return (sheets as any).sheet; + return sheets.sheet; } -export function addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { +export function addStyleSheetRule(sheet: CSSStyleSheet | null, selector: string, css: string | {[key:string]: string}, selectorPrefix = '.') { const propText = typeof css === 'string' ? css : Object.keys(css) .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p])) .join(';'); - return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); + return sheet?.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length); } -export function removeStyleSheetRule(sheet: any, rule: number) { - if (sheet.rules.length) { +export function removeStyleSheetRule(sheet: CSSStyleSheet|null, rule: number) { + if (sheet?.rules.length) { sheet.removeRule(rule); return true; } return false; } -export function clearStyleSheetRules(sheet: any) { - if (sheet.rules.length) { +export function clearStyleSheetRules(sheet: CSSStyleSheet|null) { + if (sheet?.rules.length) { numberRange(sheet.rules.length).map(() => sheet.removeRule(0)); return true; } diff --git a/src/client/documents/DocFromField.ts b/src/client/documents/DocFromField.ts index b65bbbdf5..9acb9c225 100644 --- a/src/client/documents/DocFromField.ts +++ b/src/client/documents/DocFromField.ts @@ -1,5 +1,4 @@ import { Doc, DocListCast } from '../../fields/Doc'; -import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { StrCast } from '../../fields/Types'; import { AudioField, ImageField, PdfField, VideoField } from '../../fields/URLField'; @@ -12,7 +11,17 @@ export function ResetLayoutFieldKey(doc: Doc, fieldKey: string) { doc.layout = StrCast(doc.layout).replace(/={'.*'}/, `={'${fieldKey}'}`); return doc; } -export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { +/** + * Creates a new document based on the type of (and containing the) data in the specified field of an existing document. + * If the field contains a list, then it may be useful to specify a classProto to indicate the type of + * collection Doc that gets created. + * @param target document to retrive field from + * @param fieldKey field key to retrieve + * @param classProto optionally a class proto to set on the new document + * @param options metadata configuration for new document + * @returns + */ +export function DocumentFromField(target: Doc, fieldKey: string, classProto?: Doc, options?: DocumentOptions): Doc | undefined { const field = target[fieldKey]; const resolved = options ?? {}; const nonDocFieldToDoc = () => { @@ -20,12 +29,11 @@ export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, op if (field instanceof VideoField) return Docs.Create.VideoDocument(field.url.href, resolved); if (field instanceof PdfField) return Docs.Create.PdfDocument(field.url.href, resolved); if (field instanceof AudioField) return Docs.Create.AudioDocument(field.url.href, resolved); - if (field instanceof InkField) return Docs.Create.InkDocument(field.inkData, resolved); if (field instanceof List && field[0] instanceof Doc) return Docs.Create.StackingDocument(DocListCast(field), resolved); return Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved }); }; const created = field instanceof Doc ? field : ResetLayoutFieldKey(nonDocFieldToDoc(), fieldKey); created.title = fieldKey; - proto && created.proto && (created.proto = Doc.GetProto(proto)); + classProto && created.proto && (created.proto = classProto); return created; } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ea5eca804..fb5cb27b2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -5,7 +5,7 @@ import { reaction } from 'mobx'; import { basename } from 'path'; import { ClientUtils, OmitKeys } from '../../ClientUtils'; import { DateField } from '../../fields/DateField'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, CreateLinkToActiveAudio, Doc, FieldType, Opt, updateCachedAcls } from '../../fields/Doc'; +import { CreateLinkToActiveAudio, Doc, FieldType, Opt, updateCachedAcls } from '../../fields/Doc'; import { Initializing } from '../../fields/DocSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField } from '../../fields/InkField'; @@ -838,18 +838,7 @@ export namespace Docs { return linkDoc; } - export function InkDocument( - points: PointData[], - options: DocumentOptions = {}, - strokeWidth = ActiveInkWidth(), - color = ActiveInkColor(), - strokeBezier = ActiveInkBezierApprox(), - fillColor = ActiveFillColor(), - arrowStart = ActiveArrowStart(), - arrowEnd = ActiveArrowEnd(), - dash = ActiveDash(), - isInkMask = ActiveIsInkMask() - ) { + export function InkDocument(points: PointData[], options: DocumentOptions = {}, strokeWidth: number, color: string, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, isInkMask: boolean) { const ink = InstanceFromProto(Prototypes.get(DocumentType.INK), '', { title: 'ink', ...options }); const I = Doc.GetProto(ink); // I.layout_hideOpenButton = true; // don't show open full screen button when selected @@ -865,6 +854,7 @@ export namespace Docs { I.text_align = 'center'; I.rotation = 0; I.defaultDoubleClick = 'ignore'; + I.keepZWhenDragged = true; I.author_date = new DateField(); I.acl_Guest = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; // I.acl_Override = SharingPermissions.Unset; diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index d3c10f9f4..278931cdd 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -29,7 +29,7 @@ export enum ColorScheme { } @observer -export class SettingsManager extends React.Component<{}> { +export class SettingsManager extends React.Component<object> { // eslint-disable-next-line no-use-before-define public static Instance: SettingsManager; static _settingsStyle = addStyleSheet(); @@ -123,7 +123,7 @@ export class SettingsManager extends React.Component<{}> { 'change color scheme' ); - constructor(props: {}) { + constructor(props: object) { super(props); makeObservable(this); SettingsManager.Instance = this; diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 1337d271f..cc0366c5b 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -11,6 +11,7 @@ export class SnappingManager { return SnappingManager._manager ?? new SnappingManager(); } + @observable _longPress = false; @observable _shiftKey = false; @observable _ctrlKey = false; @observable _metaKey = false; @@ -43,6 +44,7 @@ export class SnappingManager { public static get HorizSnapLines() { return this.Instance._horizSnapLines; } // prettier-ignore public static get VertSnapLines() { return this.Instance._vertSnapLines; } // prettier-ignore + public static get LongPress() { return this.Instance._longPress; } // prettier-ignore public static get ShiftKey() { return this.Instance._shiftKey; } // prettier-ignore public static get CtrlKey() { return this.Instance._ctrlKey; } // prettier-ignore public static get MetaKey() { return this.Instance._metaKey; } // prettier-ignore @@ -58,6 +60,7 @@ export class SnappingManager { public static get PropertiesWidth(){ return this.Instance._propertyWid; } // prettier-ignore public static get PrintToConsole() { return this.Instance._printToConsole; } // prettier-ignore + public static SetLongPress = (press: boolean) => runInAction(() => {this.Instance._longPress = press}); // prettier-ignore public static SetShiftKey = (down: boolean) => runInAction(() => {this.Instance._shiftKey = down}); // prettier-ignore public static SetCtrlKey = (down: boolean) => runInAction(() => {this.Instance._ctrlKey = down}); // prettier-ignore public static SetMetaKey = (down: boolean) => runInAction(() => {this.Instance._metaKey = down}); // prettier-ignore diff --git a/src/client/util/request-image-size.ts b/src/client/util/request-image-size.ts index 0f98a2710..48cb6e3a5 100644 --- a/src/client/util/request-image-size.ts +++ b/src/client/util/request-image-size.ts @@ -54,6 +54,7 @@ module.exports = function requestImageSize(options: any) { } } catch (err) { /* empty */ + console.log("Error: ", err) } }); diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 2f26bdaef..e3e252593 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -4,25 +4,23 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; +import { Doc, Opt } from '../../fields/Doc'; +import { InkData, InkField, InkTool } from '../../fields/InkField'; +import { NumCast } from '../../fields/Types'; import { ActiveArrowEnd, ActiveArrowScale, ActiveArrowStart, ActiveDash, - ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, - Doc, - Opt, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, -} from '../../fields/Doc'; -import { InkData, InkField, InkTool } from '../../fields/InkField'; -import { NumCast } from '../../fields/Types'; +} from './nodes/DocumentView'; // import MobileInkOverlay from '../../mobile/MobileInkOverlay'; import { Gestures } from '../../pen-gestures/GestureTypes'; import { GestureUtils } from '../../pen-gestures/GestureUtils'; @@ -32,7 +30,7 @@ import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { Transform } from '../util/Transform'; import './GestureOverlay.scss'; import { ObservableReactComponent } from './ObservableReactComponent'; -import { DocumentView } from './nodes/DocumentView'; +import { ActiveFillColor, DocumentView } from './nodes/DocumentView'; export enum ToolglassTools { InkToText = 'inktotext', diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 31d88fb87..ef1bcfb64 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -555,16 +555,16 @@ export class MainView extends ObservableReactComponent<{}> { private longPressTimer: NodeJS.Timeout | undefined; globalPointerClick = action(() => { this.longPressTimer && clearTimeout(this.longPressTimer); - DocumentView.LongPress = false; + SnappingManager.SetLongPress(false); }); globalPointerMove = action((e: PointerEvent) => { if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer); }); globalPointerDown = action((e: PointerEvent) => { - DocumentView.LongPress = false; + SnappingManager.SetLongPress(false); this.longPressTimer = setTimeout( action(() => { - DocumentView.LongPress = true; + SnappingManager.SetLongPress(true); }), 1000 ); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index b7f8a3170..8c100f238 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -152,7 +152,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & case StyleProp.DocContents: return undefined; case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; case StyleProp.Opacity: return componentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); - case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], StrCast(Doc.UserDoc().fontColor, color())); + case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], isCaption ? lightOrDark(backgroundCol()) : StrCast(Doc.UserDoc().fontColor, color())); case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 27c85533f..38f681e87 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -17,6 +17,7 @@ import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView } from './CollectionSubView'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @observer diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 73179a266..2a36e96bf 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -58,7 +58,7 @@ export class CollectionDockingView extends CollectionSubView() { return this._goldenLayout._maximisedItem !== null; } private _goldenLayout: any = null; - static _highlightStyleSheet: any = addStyleSheet(); + static _highlightStyleSheet = addStyleSheet(); constructor(props: any) { super(props); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 3eb3008c4..b2f0280a5 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -17,7 +17,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { RichTextField } from '../../../fields/RichTextField'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; @@ -47,7 +47,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { private _docBtnRef = React.createRef<HTMLDivElement>(); - constructor(props: any) { + constructor(props: CollectionMenuProps) { super(props); makeObservable(this); CollectionMenu.Instance = this; @@ -279,8 +279,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { - button.target_childFilters = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as any as ObjectField) : undefined; - button.target_searchFilterDocs = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs as any as ObjectField) : undefined; + button.target_childFilters = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as ObjectField) : undefined; + button.target_searchFilterDocs = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs) : undefined; } }, }; @@ -338,15 +338,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu this._currentKey = this._currentKey || (this._buttonizableCommands?.length ? this._buttonizableCommands[0]?.title : ''); }); - @undoBatch - viewChanged = (e: React.ChangeEvent) => { - const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto); - target._type_collection = (e.target as any).selectedOptions[0].value; - }; - - commandChanged = (e: React.ChangeEvent) => { + commandChanged = (e: React.ChangeEvent<HTMLSelectElement>) => { runInAction(() => { - this._currentKey = (e.target as any).selectedOptions[0].value; + this._currentKey = e.target.selectedOptions[0].value; }); }; @@ -485,7 +479,7 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi }; @action - onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { + onKeyChange = (e: React.FormEvent<Element>, { newValue }: { newValue: string }) => { this._currentKey = newValue; }; @@ -538,7 +532,6 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi autosuggestProps: { inputProps: { value: this._currentKey, - // @ts-ignore onChange: this.onKeyChange, }, getSuggestionValue: this.getSuggestionValue, @@ -733,7 +726,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu onChange={this.changeCompactType} value={StrCast(this.document.gridStartCompaction, StrCast(this.document.gridCompaction))}> {['vertical', 'horizontal', 'none'].map(type => ( - <option className="collectionGridViewChrome-viewOption" onPointerDown={stopPropagation} value={type}> + <option key={type} className="collectionGridViewChrome-viewOption" onPointerDown={stopPropagation} value={type}> {this.resize ? type[0].toUpperCase() + type.substring(1) : 'Compact: ' + type} </option> ))} diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index c39df2c76..285598600 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -52,7 +52,6 @@ export type collectionTreeViewProps = { export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() { public static AddTreeFunc = 'addTreeFolder(this.embedContainer)'; private _treedropDisposer?: DragManager.DragDropDisposer; - private _mainEle?: HTMLDivElement; private _titleRef?: HTMLDivElement | HTMLInputElement | null; private _disposers: { [name: string]: IReactionDisposer } = {}; private _isDisposing = false; // notes that instance is in process of being disposed @@ -83,8 +82,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _titleHeight = 0; // height of the title bar - MainEle = () => this._mainEle; - // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => { @@ -134,7 +131,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree }; protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); - this._mainEle = ele; if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this)); }; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index b82421e6b..f69aea2a7 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -469,14 +469,12 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return false; } - refTransform = (ref: HTMLDivElement | undefined | null) => { + refTransform = (ref: HTMLElement | undefined | null) => { if (!ref) return this.ScreenToLocalTransform(); - const { translateX, translateY } = ClientUtils.GetScreenTransform(ref); - const outerXf = ClientUtils.GetScreenTransform(this.treeView.MainEle()); - const offset = this.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY); - return this.ScreenToLocalTransform().translate(offset[0], offset[1]); + const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref); + return new Transform(-translateX, -translateY, 1).scale(1/scale); }; - docTransform = () => this.refTransform(this._dref?.ContentRef?.current); + docTransform = () => this.refTransform(this._dref?.ContentDiv); getTransform = () => this.refTransform(this._tref.current); embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1); embeddedPanelHeight = () => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b6e1fca77..5b7f09be3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -9,7 +9,8 @@ import { computedFn } from 'mobx-utils'; import * as React from 'react'; import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; -import { ActiveEraserWidth, ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, Opt } from '../../../../fields/Doc'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField'; @@ -38,7 +39,7 @@ import { ContextMenu } from '../../ContextMenu'; import { InkingStroke } from '../../InkingStroke'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; -import { DocumentView } from '../../nodes/DocumentView'; +import { ActiveFillColor, DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -87,8 +88,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } private _clusters = new CollectionFreeFormClusters(this); - private _oldWheel: any; - private _panZoomTransitionTimer: any; + private _oldWheel: HTMLDivElement | null = null; + private _panZoomTransitionTimer: NodeJS.Timeout | undefined = undefined; + private _brushtimer: NodeJS.Timeout | undefined = undefined; + private _brushtimer1: NodeJS.Timeout | undefined = undefined; private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; @@ -97,8 +100,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _disposers: { [name: string]: IReactionDisposer } = {}; private _renderCutoffData = observable.map<string, boolean>(); private _batch: UndoManager.Batch | undefined = undefined; - private _brushtimer: any; - private _brushtimer1: any; private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation. private _presEaseFunc: string = 'ease'; @@ -129,7 +130,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _eraserX: number = 0; @observable _eraserY: number = 0; @observable _showEraserCircle: boolean = false; // to determine whether the radius eraser should show - constructor(props: any) { + constructor(props: collectionFreeformViewProps) { super(props); makeObservable(this); } @@ -550,7 +551,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection _width: B.width + inkWidth, _height: B.height + inkWidth, stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore - inkWidth + inkWidth, + ActiveInkColor(), + ActiveInkBezierApprox(), + ActiveFillColor(), + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + ActiveIsInkMask() ); if (Doc.ActiveTool === InkTool.Write) { this.unprocessedDocs.push(inkDoc); @@ -634,7 +642,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection _width: B.width + inkWidth, _height: B.height + inkWidth, stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore - inkWidth + inkWidth, + ActiveInkColor(), + ActiveInkBezierApprox(), + ActiveFillColor(), + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + ActiveIsInkMask() ); }); newStrokes && this.addDocument?.(newStrokes); @@ -684,8 +699,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; }; - forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => { - this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text)); + forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData) => { + this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points))); }; onPointerMove = (e: PointerEvent) => { @@ -1639,7 +1654,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * since rendering a large collection of documents can be slow, at startup, docs are rendered in batches. * each doc's render() method will call the cutoff provider which will let the doc know if it should render itself yet, or wait */ - renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + ''))); + renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc || this.Document.isTemplateForField ? false : !this._renderCutoffData.get(doc[Id] + ''))); doEngineLayout( poolData: Map<string, PoolData>, @@ -1673,7 +1688,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .forEach(entry => elements.push({ ele: this.getChildDocView(entry[1]), - bounds: (entry[1].opacity === 0 ? { ...entry[1], width: 0, height: 0 } : { ...entry[1] }) as any, + bounds: (entry[1].opacity === 0 ? { payload:undefined, type:"", ...entry[1], width: 0, height: 0 } : { payload:undefined, type:"",...entry[1] }), inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }) ); @@ -1795,10 +1810,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection Object.values(this._disposers).forEach(disposer => disposer?.()); } - updateIcon = () => - UpdateIcon( + updateIcon = () => { + const contentDiv = this.DocumentView?.().ContentDiv; + contentDiv && UpdateIcon( this.layoutDoc[Id] + '-icon' + new Date().getTime(), - this.DocumentView?.().ContentDiv!, + contentDiv, NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._height), this._props.PanelWidth(), @@ -1813,6 +1829,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.dataDoc.icon_nativeHeight = nativeHeight; } ); + } @action onCursorMove = (e: React.PointerEvent) => { @@ -1993,7 +2010,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRender = action(() => { if (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())) { const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); - const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5; + const loadIncrement = this.Document.isTemplateDoc || this.Document.isTemplateForField ? Number.MAX_VALUE : 5; for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) { this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true); } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 7c2cfd15f..6bea53355 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -32,6 +32,7 @@ import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key: string]: ColumnType } = { diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 7730ed385..2b7de5082 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -1,25 +1,10 @@ import { Colors } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { aggregateBounds } from '../../../Utils'; -import { - ActiveFillColor, - ActiveEraserWidth, - ActiveInkColor, - ActiveInkHideTextLabels, - ActiveInkWidth, - ActiveIsInkMask, - Doc, - DocListCast, - Opt, - SetActiveFillColor, - SetActiveInkColor, - SetActiveInkHideTextLabels, - SetActiveInkWidth, - SetActiveIsInkMask, - SetEraserWidth, -} from '../../../fields/Doc'; +import { Doc, DocListCast, NumListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; +import { List } from '../../../fields/List'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { Gestures } from '../../../pen-gestures/GestureTypes'; @@ -31,13 +16,25 @@ import { GestureOverlay } from '../GestureOverlay'; import { InkingStroke } from '../InkingStroke'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; -import { DocumentView } from '../nodes/DocumentView'; +import { + ActiveEraserWidth, + ActiveFillColor, + ActiveInkColor, + ActiveInkHideTextLabels, + ActiveInkWidth, + ActiveIsInkMask, + DocumentView, + SetActiveFillColor, + SetActiveInkColor, + SetActiveInkHideTextLabels, + SetActiveInkWidth, + SetActiveIsInkMask, + SetEraserWidth, +} from '../nodes/DocumentView'; import { ImageBox } from '../nodes/ImageBox'; import { VideoBox } from '../nodes/VideoBox'; import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; -import { NumListCast } from '../../../fields/Doc'; -import { List } from '../../../fields/List'; // import { InkTranscription } from '../InkTranscription'; @@ -59,7 +56,7 @@ ScriptingGlobals.add(function setView(view: string, getSelected: boolean) { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) { const selectedViews = DocumentView.Selected(); - if (Doc.ActiveTool !== InkTool.None) { + if (Doc.ActiveTool !== InkTool.None && !selectedViews.lastElement()?.Document._layout_isSvg) { if (checkResult) { return ActiveFillColor(); } @@ -450,15 +447,19 @@ function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult? GestureOverlay.Instance.InkShape = tool as Gestures; } } else if (tool) { - if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as any)) { - Doc.UserDoc().activeEraserTool = tool; - } - // pen or eraser - if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + if (Doc.UserDoc().ActiveTool === tool) { Doc.ActiveTool = InkTool.None; } else { - Doc.ActiveTool = tool as any; - GestureOverlay.Instance.InkShape = undefined; + if ([InkTool.StrokeEraser, InkTool.RadiusEraser, InkTool.SegmentEraser].includes(tool as any)) { + Doc.UserDoc().activeEraserTool = tool; + } + // pen or eraser + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + Doc.ActiveTool = InkTool.None; + } else { + Doc.ActiveTool = tool as any; + GestureOverlay.Instance.InkShape = undefined; + } } } else { Doc.ActiveTool = InkTool.None; diff --git a/src/client/views/nodes/ChatBox/MessageComponent.scss b/src/client/views/nodes/ChatBox/MessageComponent.scss new file mode 100644 index 000000000..6fcc0e5e7 --- /dev/null +++ b/src/client/views/nodes/ChatBox/MessageComponent.scss @@ -0,0 +1,10 @@ +MessageComponent-citation { + color: lightblue; + vertical-align: super; + font-size: smaller; +} +MessageComponent-file_path { + color: lightblue; + vertical-align: baseline; + font-size: inherit; +} diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 034a38e9c..ee67dd305 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -20,6 +20,11 @@ import { DocumentView, DocumentViewProps } from './DocumentView'; import { FieldViewProps } from './FieldView'; import { OpenWhere } from './OpenWhere'; +export enum GroupActive { // flags for whether a view is activate because of its relationship to a group + group = 'group', // this is a group that is activated because it's on an active canvas, but is not part of some other group + child = 'child', // this is a group child that is activated because its containing group is activated + inactive = 'inactive', // this is a group child but it is not active +} /// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition']; @@ -274,7 +279,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (this._props.isAnyChildContentActive()) return undefined; const backColor = this.BackgroundColor; const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent'); - return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; + return isGroup ? (this._props.isDocumentActive?.() ? GroupActive.group : + this._props.isGroupActive?.() ? GroupActive.child : GroupActive.inactive) : + this._props.isGroupActive?.() ? GroupActive.child : undefined; // prettier-ignore }; localRotation = () => this._props.rotation; render() { @@ -313,6 +320,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { +ScriptingGlobals.add(function gotoFrame(doc: Doc, newFrame: number) { CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame); }); diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 093b9c004..08d9e6010 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -49,7 +49,6 @@ left: 0; height: 100%; overflow: hidden; - transition: 200ms; .beforeBox-cont { height: 100%; @@ -63,7 +62,6 @@ width: 3px; display: inline-block; background: white; - transition: 200ms; .slide-handle { position: absolute; diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index adb380f12..efaf6807a 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -30,41 +30,23 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return FieldView.LayoutString(ComparisonBox, fieldKey); } private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined]; + private _closeRef = React.createRef<HTMLDivElement>(); + @observable _inputValue = ''; + @observable _outputValue = ''; + @observable _loading = false; + @observable _errorMessage = ''; + @observable _outputMessage = ''; + @observable _animating = ''; + constructor(props: FieldViewProps) { super(props); makeObservable(this); } - @observable inputValue = ''; - @observable outputValue = ''; - @observable loading = false; - @observable errorMessage = ''; - @observable outputMessage = ''; - - @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - this.inputValue = e.target.value; - console.log(this.inputValue); - }; - - @observable _animating = ''; - - @computed get clipWidth() { - return NumCast(this.layoutDoc[this.clipWidthKey], 50); - } - get clipWidthKey() { - return '_' + this._props.fieldKey + '_clipWidth'; - } - - @computed get clipHeight() { - return NumCast(this.layoutDoc[this.clipHeightKey], 200); - } - get clipHeightKey() { - return '_' + this._props.fieldKey + '_clipHeight'; - } - componentDidMount() { this._props.setContentViewBox?.(this); } + protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { @@ -72,7 +54,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() } }; - private internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + @computed get revealOp() { return this.layoutDoc[`_${this.fieldKey}_revealOp`] as ('flip'|'hover'|undefined); } // prettier-ignore + @computed get clipWidth() { return NumCast(this.layoutDoc[`_${this.fieldKey}_clipWidth`], 50); } // prettier-ignore + set clipWidth(width: number) { this.layoutDoc[`_${this.fieldKey}_clipWidth`] = width; } // prettier-ignore + @computed get useAlternate() { return this.layoutDoc[`_${this.fieldKey}_usePath`] === 'alternate'; } // prettier-ignore + set useAlternate(alt: boolean) { this.layoutDoc[`_${this.fieldKey}_usePath`] = alt ? 'alternate' : undefined; } // prettier-ignore + + animateClipWidth = action((clipWidth: number, duration = 200 /* ms */) => { + this._animating = `all ${duration}ms`; // turn on clip animation transition, then turn it off at end of animation + setTimeout(action(() => { this._animating = ''; }), duration); // prettier-ignore + this.clipWidth = clipWidth; + }); + + internalDrop = undoable((e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { const { droppedDocuments } = dropEvent.complete.docDragData; const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocuments, this.Document, (doc: Doc | Doc[]) => this.addDoc(toList(doc).lastElement(), fieldKey)); @@ -84,45 +78,34 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return undefined; }, 'internal drop'); - private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { + registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, - action((moveEv, doubleTap) => { + action((clickEv, doubleTap) => { if (doubleTap) { this._isAnyChildContentActive = true; if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + // DocumentView.addViewRenderedCb(DocCast(this.dataDoc[this.fieldKey + '_1']), dv => { + // dv?.select(false); + // }); } }), - false, + true, undefined, - action(() => { - if (this._isAnyChildContentActive) return; - this._animating = 'all 200ms'; - // on click, animate slider movement to the targetWidth - this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth(); - // this.layoutDoc[this.clipHeightKey] = (targetWidth * 100) / this._props.PanelHeight(); - - setTimeout( - action(() => { - this._animating = ''; - }), - 200 - ); - }) + () => !this._isAnyChildContentActive && this.animateClipWidth((targetWidth * 100) / this._props.PanelWidth()) ); } }; - @action - private onPointerMove = ({ movementX }: PointerEvent) => { + onPointerMove = ({ movementX }: PointerEvent) => { const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth(); - if (width && width > 5 && width < this._props.PanelWidth()) { - this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth(); + if (width > 5 && width < this._props.PanelWidth()) { + this.clipWidth = (width * 100) / this._props.PanelWidth(); } return false; }; @@ -180,8 +163,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; docStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { - if (property === StyleProp.PointerEvents) return 'none'; - return this._props.styleProvider?.(doc, props, property); + switch (property) { + case StyleProp.PointerEvents: return 'none'; + default: return this._props.styleProvider?.(doc, props, property); + } // prettier-ignore }; moveDoc1 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); moveDoc2 = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); @@ -220,36 +205,31 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return layoutTemplateString; }; - _closeRef = React.createRef<HTMLDivElement>(); - /** * Flips a flashcard to the alternate side for the user to view. */ flipFlashcard = () => { - const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined; + this.useAlternate = !this.useAlternate; }; /** * Changes the view option to hover for a flashcard. */ - hoverFlip = (side: string | undefined) => { - if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side; + hoverFlip = (alternate: boolean) => { + if (this.revealOp === 'hover') this.useAlternate = alternate; }; /** * Creates the button used to flip the flashcards. */ @computed get overlayAlternateIcon() { - const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( <Tooltip title={<div className="dash-tooltip">flip</div>}> <div className="formattedTextBox-alternateButton" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { - console.log(this.layoutDoc[`_${this._props.fieldKey}_revealOp`]); - if (!this.layoutDoc[`_${this._props.fieldKey}_revealOp`] || this.layoutDoc[`_${this._props.fieldKey}_revealOp`] === 'flip') { + if (!this.revealOp || this.revealOp === 'flip') { this.flipFlashcard(); console.log('Print Front of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text ?? '')); console.log('Print Back of cards: ' + (RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text ?? '')); @@ -257,8 +237,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() }) } style={{ - background: usepath === 'alternate' ? 'white' : 'black', - color: usepath === 'alternate' ? 'black' : 'white', + background: this.useAlternate ? 'white' : 'black', + color: this.useAlternate ? 'black' : 'white', }}> <FontAwesomeIcon icon="turn-up" size="sm" /> </div> @@ -268,14 +248,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() @action handleRenderGPTClick = () => { // Call the GPT model and get the output - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate'; - this.outputValue = ''; - if (this.inputValue) this.askGPT(); + this.useAlternate = true; + this._outputValue = ''; + if (this._inputValue) this.askGPT(); }; @action handleRenderClick = () => { // Call the GPT model and get the output - this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined; + this.useAlternate = false; }; /** @@ -285,7 +265,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() askGPT = async (): Promise<string | undefined> => { const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text); const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text); - const queryText = questionText + ' UserAnswer: ' + this.inputValue + '. ' + rubricText; + const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText; try { const res = await gptAPICall(queryText, GPTCallType.QUIZ); @@ -293,8 +273,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() console.error('GPT call failed'); return; } - this.outputValue = res; - console.log(res); + this._outputValue = res; } catch (err) { console.error('GPT call failed'); } @@ -355,7 +334,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() ); if (this.Document._layout_isFlashcard) { - const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0; + const side = this.useAlternate ? 1 : 0; // add text box to each side when comparison box is first created if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] === 'empty')) { @@ -383,17 +362,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {/* {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)} */} <div className="input-box"> <textarea - value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.outputValue : this.inputValue} - onChange={this.handleInputChange} - readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'} + value={this.useAlternate ? this._outputValue : this._inputValue} + onChange={action(e => { + this._inputValue = e.target.value; + })} + readOnly={this.useAlternate} /> </div> - <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'none' : 'flex' }}> + <div className="submit-button" style={{ display: this.useAlternate ? 'none' : 'flex' }}> <button type="button" onClick={this.handleRenderGPTClick}> Submit </button> </div> - <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}> + <div className="submit-button" style={{ display: this.useAlternate ? 'flex' : 'none' }}> <button type="button" onClick={this.handleRenderClick}> Edit Your Response </button> @@ -407,12 +388,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */ style={{ display: 'flex', flexDirection: 'column' }} - onMouseEnter={() => { - this.hoverFlip('alternate'); - }} - onMouseLeave={() => { - this.hoverFlip(undefined); - }}> + onMouseEnter={() => this.hoverFlip(true)} + onMouseLeave={() => this.hoverFlip(false)}> {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)} {this.overlayAlternateIcon} </div> @@ -430,9 +407,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() className="slide-bar" style={{ left: `calc(${this.clipWidth + '%'} - 0.5px)`, + transition: this._animating, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this._props.PanelWidth() - 5) / this._props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => this.registerSliding(e, this._props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index a1deb1625..d2e82284e 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -18,6 +18,7 @@ import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; import './Chart.scss'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore interface TableBoxProps { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 7a1f94948..ce7cfa5f4 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -43,6 +43,7 @@ import { ObserverJsxParser } from '../ObservableReactComponent'; import { PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { ViewBoxInterface } from '../ViewBoxInterface'; +import { GroupActive } from './CollectionFreeFormDocumentView'; import { DocumentContentsView } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; @@ -52,7 +53,7 @@ import { OpenWhere, OpenWhereMod } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails/PresEnums'; import SpringAnimation from './trails/SlideEffect'; -import { SpringSettings, SpringType, springMappings } from './trails/SpringUtils'; +import { SpringType, springMappings } from './trails/SpringUtils'; interface Window { MediaRecorder: MediaRecorder; @@ -153,7 +154,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document @computed get disableClickScriptFunc() { const onScriptDisable = this._props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; - return (DocumentView.LongPress || + return (SnappingManager.LongPress || onScriptDisable === 'always' || (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.()))); // prettier-ignore } @@ -297,17 +298,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document e.stopPropagation(); }; onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; - const documentView = this._docView; - if (documentView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return; + if (this._docView && !this.Document.ignoreClick && this._props.renderDepth >= 0 && ClientUtils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document); const scriptProps = { this: this.Document, _readOnly_: false, scriptContext: this._props.scriptContext, - documentView, + documentView: this._docView, clientX: e.clientX, clientY: e.clientY, shiftKey: e.shiftKey, @@ -317,44 +316,39 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; if (this._doubleTap) { const defaultDblclick = this._props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; - if (this.onDoubleClickHdlr?.script) { - UndoManager.RunInBatch(() => this.onDoubleClickHdlr.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title); - } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') { - UndoManager.RunInBatch(() => this._props.addDocTab(this.Document, OpenWhere.lightboxAlways), 'double tap'); - DocumentView.DeselectAll(); - Doc.UnBrushDoc(this.Document); - } else { - this._singleClickFunc?.(); - } + undoable(() => { + if (this.onDoubleClickHdlr?.script) { + this.onDoubleClickHdlr.script.run(scriptProps, console.log).result?.select && this._props.select(false); + } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') { + this._props.addDocTab(this.Document, OpenWhere.lightboxAlways); + DocumentView.DeselectAll(); + Doc.UnBrushDoc(this.Document); + } else this._singleClickFunc?.(); + }, 'on double click: ' + this.Document.title)(); this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = undefined; this._singleClickFunc = undefined; } else { - let clickFunc: undefined | (() => any); - if (!this.disableClickScriptFunc && this.onClickHdlr?.script) { - clickFunc = undoable(() => { - this.onClickHdlr?.script.run(scriptProps, console.log).result?.select && this._props.select(false); - }, 'click ' + this.Document.title); - } else { - // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part - if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) { - stopPropagate = false; - } - preventDefault = false; - } - const sendToBack = e.altKey ? () => documentView._props.bringToFront?.(this.Document, true) : undefined; + const sendToBack = e.altKey ? () => this._props.bringToFront?.(this.Document, true) : undefined; const selectFunc = () => { + !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document); // selecting a view that is part of a template proxies the selection back to the root of the template const templateRoot = !(e.ctrlKey || e.button > 0) && this._props.docViewPath?.().reverse().find(dv => !dv._props.TemplateDataDocument); // prettier-ignore (templateRoot || this._docView)?.select(e.ctrlKey || e.shiftKey, e.metaKey); }; - this._singleClickFunc = clickFunc ?? sendToBack ?? selectFunc; - const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; - if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { + const clickFunc = this.onClickFunc?.()?.script ? () => (this.onClickFunc?.()?.script.run(scriptProps, console.log).result as Opt<{ select: boolean }>)?.select && this._props.select(false) : undefined; + if (!clickFunc) { + // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + if (this.layoutDoc.onDragStart && !(e.ctrlKey || e.button > 0)) stopPropagate = false; + preventDefault = false; + } + this._singleClickFunc = undoable(clickFunc ?? sendToBack ?? selectFunc, 'click: ' + this.Document.title); + const waitForDblClick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; + if ((clickFunc && waitForDblClick !== 'never') || waitForDblClick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); // eslint-disable-next-line no-use-before-define - } else if (!DocumentView.LongPress) { + } else if (!SnappingManager.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; } @@ -365,9 +359,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }); onPointerDown = (e: React.PointerEvent): void => { - if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return; + if (this._props.isGroupActive?.() === GroupActive.child && !this._props.isDocumentActive?.()) return; // eslint-disable-next-line no-use-before-define - this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000); + this._longPressSelector = setTimeout(() => SnappingManager.LongPress && this._props.select(false), 1000); if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView; this._downX = e.clientX; @@ -418,7 +412,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } // eslint-disable-next-line no-use-before-define - if (DocumentView.LongPress) e.preventDefault(); + if (SnappingManager.LongPress) e.preventDefault(); }; toggleFollowLink = undoable((): void => { @@ -975,8 +969,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * @returns a function that will wrap a JSX animation element wrapping any JSX element */ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) { - let dir = (presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) as PresEffectDirection; - const transitionTime = presEffectDoc?.presentation_transition ? NumCast(presEffectDoc?.presentation_transition) : 500; + const dir = ((presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection) || PresEffectDirection.Center) as PresEffectDirection; + const duration = Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)); const effectProps = { left: dir === PresEffectDirection.Left, right: dir === PresEffectDirection.Right, @@ -984,26 +978,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document bottom: dir === PresEffectDirection.Bottom, opposite: true, delay: 0, - duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)), + duration, }; const timing = StrCast(presEffectDoc?.presentation_effectTiming); - let timingConfig: SpringSettings | undefined; - if (timing) { - timingConfig = JSON.parse(timing); - } - - if (!timingConfig) { - timingConfig = { - type: SpringType.GENTLE, - ...springMappings.gentle, - }; - } - - if (!dir) { - dir = PresEffectDirection.Center; - } - + const timingConfig = (timing ? JSON.parse(timing) : undefined) ?? { + type: SpringType.GENTLE, + ...springMappings.gentle, + }; switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) { case PresEffect.Expand: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Expand} springSettings={timingConfig}>{renderDoc}</SpringAnimation> case PresEffect.Flip: return <SpringAnimation doc={root} startOpacity={0} dir={dir} presEffect={PresEffect.Flip} springSettings={timingConfig}>{renderDoc}</SpringAnimation> @@ -1085,15 +1067,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. ) => Promise<void>; public static linkCommonAncestor: (link: Doc) => DocumentView | undefined; - // pin func + /** + * Pins a Doc to the current presentation trail. (see TabDocView for implementation) + */ public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void; - // gesture - public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to. - // media playing - @observable public static CurrentlyPlaying: DocumentView[] = []; + /** + * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to. + */ + public static DownDocView: DocumentView | undefined; public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore - public ContentRef = React.createRef<HTMLDivElement>(); private _htmlOverlayEffect: Opt<Doc>; private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; @@ -1124,7 +1107,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { @observable private _htmlOverlayText: Opt<string> = undefined; @observable private _isHovering = false; @observable private _selected = false; - @observable public static LongPress = false; + @observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing @computed private get shouldNotScale() { return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.(); @@ -1471,7 +1454,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" - ref={this.ContentRef} style={{ transform: `translate(${this.centeringX}px, ${this.centeringY}px)`, width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`, @@ -1568,6 +1550,56 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } } +export function ActiveFillColor(): string { + const dv = DocumentView.Selected().lastElement() ?.Document._layout_isSvg ? DocumentView.Selected().lastElement() : undefined; + return StrCast(dv?.Document.fillColor, StrCast(ActiveInkPen()?.activeFillColor, "")); +} // prettier-ignore +export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore +export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore +export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore +export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore +export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore +export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore +export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore +export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore +export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore +export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore +export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); } // prettier-ignore + +export function SetActiveInkWidth(width: string): void { + !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); +} +export function SetActiveBezierApprox(bezier: string): void { + ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); +} +export function SetActiveInkColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeInkColor = value); +} +export function SetActiveIsInkMask(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); +} +export function SetActiveInkHideTextLabels(value: boolean) { + ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); +} +export function SetActiveFillColor(value: string) { + ActiveInkPen() && (ActiveInkPen().activeFillColor = value); +} +export function SetActiveArrowStart(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); +} +export function SetActiveArrowEnd(value: string) { + ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); +} +export function SetActiveArrowScale(value: number) { + ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); +} +export function SetActiveDash(dash: string): void { + !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); +} +export function SetEraserWidth(width: number): void { + ActiveInkPen() && (ActiveInkPen().eraserWidth = width); +} + // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function DocFocusOrOpen(docIn: Doc, optionsIn?: FocusViewOptions, containingDoc?: Doc) { return DocumentView.FocusOrOpen(docIn, optionsIn, containingDoc); diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index e4b3a1b9b..68c313480 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -448,6 +448,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options)); + renderedPixelDimensions = async () => { + const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] }); + return { width, height }; + }; + savedAnnotations = () => this._savedAnnotations; render() { TraceMobx(); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 7a89b143b..8db68ddfe 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -655,9 +655,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then((pdf: any) => { - PDFBox.pdfcache.set(href, (this._pdf = pdf)); - }); + PDFBox.pdfpromise.get(href)?.then( + action((pdf: any) => { + PDFBox.pdfcache.set(href, (this._pdf = pdf)); + }) + ); } } return pdfView ?? this.renderTitleBox; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 8835ea5e7..da947face 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -45,6 +45,7 @@ import { LinkInfo } from './LinkDocPreview'; import { OpenWhere } from './OpenWhere'; import './WebBox.scss'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { CreateImage } = require('./WebBoxRenderer'); @observer diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9f2a9b8e1..5b435e44a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -87,9 +87,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection static _globalHighlightsCache: string = ''; static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); - static _highlightStyleSheet: any = addStyleSheet(); - static _bulletStyleSheet: any = addStyleSheet(); - static _userStyleSheet: any = addStyleSheet(); + static _highlightStyleSheet = addStyleSheet(); + static _bulletStyleSheet = addStyleSheet(); + static _userStyleSheet = addStyleSheet(); static _hadSelection: boolean = false; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); @@ -384,7 +384,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } } else { - const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data!; + const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data; if (jsonstring) { const json = JSON.parse(jsonstring); json.selection = state.toJSON().selection; @@ -1925,11 +1925,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}> <ComponentTag // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - ref={this._sidebarTagRef as any} + ref={this._sidebarTagRef} setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 2d6ed57d5..e8bcca638 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -24,18 +24,16 @@ import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { LinkInfo } from '../nodes/LinkDocPreview'; import { PDFBox } from '../nodes/PDFBox'; -import { ComparisonBox } from '../nodes/ComparisonBox'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { StyleProp } from '../StyleProp'; import { AnchorMenu } from './AnchorMenu'; import { Annotation } from './Annotation'; import { GPTPopup } from './GPTPopup/GPTPopup'; -import { Docs } from '../../documents/Documents'; import './PDFViewer.scss'; // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. -Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.2.67/build/pdf.worker.mjs'; +Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.mjs'; interface IViewerProps extends FieldViewProps { pdfBox: PDFBox; @@ -58,7 +56,7 @@ interface IViewerProps extends FieldViewProps { */ @observer export class PDFViewer extends ObservableReactComponent<IViewerProps> { - static _annotationStyle: any = addStyleSheet(); + static _annotationStyle = addStyleSheet(); constructor(props: IViewerProps) { super(props); @@ -550,7 +548,8 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; - return isInk ? 'visiblePainted' : 'all'; + if (isInk) return 'visiblePainted'; + //return isInk ? 'visiblePainted' : 'all'; } return this._props.styleProvider?.(doc, props, property); }; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 1b3d963e8..72ec16b42 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -174,53 +174,6 @@ export function updateCachedAcls(doc: Doc) { return undefined; } -export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore -export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore -export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ''); } // prettier-ignore -export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore -export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore -export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore -export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore -export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore -export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore -export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore -export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore -export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); } // prettier-ignore - -export function SetActiveInkWidth(width: string): void { - !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); -} -export function SetActiveBezierApprox(bezier: string): void { - ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier); -} -export function SetActiveInkColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeInkColor = value); -} -export function SetActiveIsInkMask(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value); -} -export function SetActiveInkHideTextLabels(value: boolean) { - ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value); -} -export function SetActiveFillColor(value: string) { - ActiveInkPen() && (ActiveInkPen().activeFillColor = value); -} -export function SetActiveArrowStart(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowStart = value); -} -export function SetActiveArrowEnd(value: string) { - ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value); -} -export function SetActiveArrowScale(value: number) { - ActiveInkPen() && (ActiveInkPen().activeArrowScale = value); -} -export function SetActiveDash(dash: string): void { - !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash); -} -export function SetEraserWidth(width: number): void { - ActiveInkPen() && (ActiveInkPen().eraserWidth = width); -} - @scriptingGlobal @Deserializable('Doc', updateCachedAcls, ['id']) export class Doc extends RefField { diff --git a/src/server/# Server Architecture.md b/src/server/# Server Architecture.md new file mode 100644 index 000000000..27d5a1a1a --- /dev/null +++ b/src/server/# Server Architecture.md @@ -0,0 +1,87 @@ +# Server Architecture + +## index.ts + +This file launches the server at port `1050`. After executing functions that need to occur before the server starts and [configuring the server](#server_initializationts), it registers all of the server's routes. Some of these it does directly, like `"/home"` which serves the user the appropriate HTML file, and others it does indirectly by instantiating the API manager responsible for registering it. Finally, it sets the web socket and the server listening on their respective ports. + +Note that if the server is running in release mode, this is where the `DashSessionAgent` is launched. This is essentially a separate process that monitors the server and can perform operations like backing up the database, sending a zipped copy of the database to a given email address and monitoring / restarting the server process in the event of an uncaught exception. + +## server_initialization.ts + +Here, we build the server object from the ground up, customizing it with `middleware`. You can think of middleware as plugins that perform useful transformations or inject helpful functionality when the server is serving its routes. This mainly includes things like authenticating a user for security, establishing sessions so that a user doesn't have to log in every time they launch Dash, invoking utilities that parse the object of requests from their stringified form in which they were sent over the network, etc. + +Here we also register the authentication routes. These don't need the [RouteManager](#RouteManagerts) security abstraction since a user won't be logged in when trying to access `"/login"` (i.e. they don't need to be protected). Note that the actual functionality for these routes is contained in [this file](#AuthenticationManagerts). + +## websocket.ts + +This file launches the web socket at port `4321`. While the server is sufficient for unidirectional requests, sometimes bidirectional communication between clients and the server is required. One such example in Dash is the updating of a field: since multiple clients might care about its value, we'd like to be able to broadcast to each client the change in real time. This is where the [web socket](https://blog.teamtreehouse.com/an-introduction-to-websockets) comes in. + +Thus, this file primarily sets up those bidirectional routes, or events. When the client socket emits an event, it will trigger the handler (invoke the code) registered to that particular event on the server. + +## ApiManager.ts + +This is a very lightweight class that just provides a template for a logical partition that encompasses a subsection of the server's functionality. For example, I might want to have all the routes that have to do with sending and receiving user information to be written in and registered by a `UserManager`. Or, maybe I want to have everything pertaining to uploading media content to the server to exist in a silo defined by `UploadManager`. + +For context, before this system, every route was defined in [index.ts](#indexts), which had become a rather long mess of a file. So, while both the `UserManager` and `UploadManager` register their routes in the same, single server, they logically separate code into searchable, well-defined files. + +## Passport.ts + +If we just used the `Express` server out of the box, we'd have no way of controlling who accesses what routes. `PassportJS` is a node module (actually, also an example of [middleware](#server_initializationts)). Take a look at [official documentation](http://www.passportjs.org/docs/) for a more in-depth explanation. Practically, it adds a user property to the http request object, and as you can see in [RouteManager.ts](#RouteManagerts) we only execute the route if the user object exists, indicating that a user is indeed logged in. + +## RouteManager.ts + +With the Express server, it's very easy to add a route. You just call `server.get("/myroute", () => console.log("do something here in the handler"));` to set up a get request, for example. However, this doesn't take advantage of [PassportJS](#Passportts) adding authentication information to the request object. Instead, the server will serve this route blindly, no matter who tries to access it, and this is a problem. Therefore, we'd like every single route to first check if a user is logged in before we carry it out, if that matters to us (and in most cases, it does). It would be a pretty inefficient to write that check in the handler for any and all new routes. So, we wrote an abstraction to add some convenience to the above security implementation. There's a wrapper that exists around those server route registration calls that basically takes in the same information, but registers as the handler not the core developer-written handler, but instead a function that inspects the request object's user property. If the handler needs to be secure and there is a user, or there is no user and the handler doesn't need to be secure, it invokes it. Otherwise, it redirects the caller to `"/login"`. + +Another benefit of handling things this way is that we can enforce the format of routes, and that's the second part of the handler. It records any malformed or duplicate route registrations, and won't let you start Dash until the formatting or conflicts are resolved. + +## DashUserModel.ts + +This file effectively specifies the model for a Dash user. The `userSchema` is the meat and potatoes here, and is only mirrored by the `DashUserModel` type. It's the schema, providing easy updating of the underlying database contents, that's used to instantiate the actual mongoose model, and it also handles the middleware conversion of the initially plain text password to the hashed, stored result. The mongoose model, used primarily in [Passport.ts](#passportts) and [AuthenticationManager.ts](#AuthenticationManagerts), has helpful functions for finding a user matching a particular query in the database without having to directly interact with the `users` MongoDB collection. + +## AuthenticationManager.ts + +This is where all the log in, sign up and password recovery logic lives, invoked when the relevant pages whose layouts are defined by [.pug](#pug) templates submit forms to the relevant routes on the server. These requests are what actually contain, for example, the username and password that the user submitted, either in signing up or logging in, and thus these are what are parsed by [PassportJS](#Passportts) for authentication (during log in), or written to the database via manipulations of the [DashUserModel](#DashUserModelts) (during sign up). + +## ActionUtilities.ts + +This file contains a bunch of potentially useful but, as of this writing, largely unused utility functions for server side operations, like simple convenience wrappers around reading and writing text files, automatically bookending the execution of a function with logs to the server, data conversion functions and even dispatching an email programmatically (sent from a dummy gmail account `brownptcdash@gmail.com`. + +## .pug + +This [file format](https://pugjs.org/api/getting-started.html) is actually pretty interesting - it's a substantially streamlined version of HTML that lets you define webpage layouts (that are maybe not quite as sophisticated or interactive as those backed by React components) very quickly. All of the individual .pug files extend `layout.pug`, which ensures that each one contains the appropriate, consistent header information as well as a common style sheet. Importantly, these pages are rendered by invoking `render` on an Express.response object, with the format `res.render("path/to/target.pug", { var1: "define variables referenced in target.pug", var2: "from TypeScript code" }`. Take a look at the `"/activity"` route in `UserManager.ts` and `user_activity.pug` for an example of defining and referencing variables. + +# Externals + +## Environment Variables + +### Why? + +Using an external [API](https://www.freecodecamp.org/news/what-is-an-api-in-english-please-b880a3214a82/) can be a fantastic and interesting way to add depth and maturity to Dash. For example, as of this writing we already use some features of Google and Microsoft Cognitive Services APIs for either integrating their proprietary data formats with our own, or providing mature machine learning driven analysis tools that we simply don't have the time or resources to develop in house. But often times, since these services either are paid or at least require us to connect them to a single billing account, they require that we create and use an [API key](https://rapidapi.com/blog/api-glossary/api-key/) to identify our interactions with these APIs. + +These keys are considered sensitive information, since anyone with the key can impersonate you and even rack up huge charges on your billing account. We don't necessarily even want these to go onto GitHub, our secure private repo, so we definitely don't want to use these hard coded values in our TypeScript code: when it's transpiled and sent to the browser, anyone can open up the developer inspector, search for 'API_KEY' and probably instantly find it embedded in and steal it from your transpiled source code. And yet, we need some way to easily interact with these values! + +### What? + +This is where environment variables step up to the plate: we can define a file called `.env` in the root of our project directory (and add it to the `.gitignore` to be extra safe!). This file is a key-value mapping with a specific format (_no spaces or quoted strings within an entry, with a single entry on each line_): + +`key1=value1` +`key2=value2` +`...` +`GOOGLE_MAPS_API_KEY=319440934820394` + +When the _server_ process starts, it's backed by a file system and can (and always will if a `.env` file is present) read in every key-value entry in `.env` and store them at `process.env.key1`, `process.env.key2` and `process.env.GOOGLE_MAPS_API_KEY`. At runtime, these keys will evaluate, in code, to the stringified version whatever is put on the right hand side of the equals sign in the `.env` file entry. So if you need to refer to a piece of sensitive information from the server, it's trivial. Your work is done. + +### Complications and Solutions + +But this is of no help if we, as we often do, need to reference sensitive authorization keys, etc. from the client. Since it is not backed by a NodeJS process (after it's transpiled and sent to the browser, it's then executed by the browser) and, since it's running on the _client_ machine which cannot read from the _server_ machine, it might seem like we'd be out of luck. For a while, I thought we were. + +But, you can add a [plugin to Webpack](https://webpack.js.org/plugins/define-plugin/) (see the entry for this) that will read in the environment variables from the server and embed them securely to be accessed in the client side, still from `process.env.GOOGLE_MAPS_API_KEY`, for example. Note that since our dependencies inject lots of (to us) unneeded environment variables into our server process, we can choose to only transfer Dash-specific environment variables to the client by (arbitrarily) prepending them with `_CLIENT_` in `.env`, and then, in `webpack.config.js`, choosing to only copy over entries with that prefix. For example, say that I wanted to be able to reference `key1` and `GOOGLE_MAPS_API_KEY` on the client side, and only needed to use `key2` on the server. My .env would look like this: + +`_CLIENT_key1=value1` +`key2=value2` +`...` +`_CLIENT_GOOGLE_MAPS_API_KEY=319440934820394` + +Note that if you have an entry that you want to use `key1` on both the client and server sides, since the Webpack processing removes the `_CLIENT_` only for the client access, you would call `process.env._CLIENT_key1` on the server, and `process.env.key1` on the client. + +And that's pretty much all there is to it! Search for `process.env` in `CognitiveServices.ts` and `CollectionMapView.tsx` for examples of retrieving these variables on the client side. |