aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/util/CurrentUserUtils.ts13
-rw-r--r--src/client/util/DragManager.ts3
-rw-r--r--src/client/util/DropConverter.ts72
-rw-r--r--src/client/util/InteractionUtils.tsx240
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx282
-rw-r--r--src/client/util/SelectionManager.ts3
-rw-r--r--src/client/views/DocComponent.tsx14
-rw-r--r--src/client/views/DocumentDecorations.tsx8
-rw-r--r--src/client/views/InkStroke.scss25
-rw-r--r--src/client/views/InkStrokeProperties.ts1
-rw-r--r--src/client/views/InkingStroke.tsx20
-rw-r--r--src/client/views/MainView.tsx7
-rw-r--r--src/client/views/StyleProvider.tsx9
-rw-r--r--src/client/views/_nodeModuleOverrides.scss3
-rw-r--r--src/client/views/collections/CollectionMenu.tsx44
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx220
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx53
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewDivider.tsx41
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx17
-rw-r--r--src/client/views/collections/TabDocView.tsx39
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss9
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx55
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx64
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx17
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx1
-rw-r--r--src/client/views/nodes/FieldView.tsx11
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx272
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx119
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx45
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx4
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx358
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx13
-rw-r--r--src/client/views/topbar/TopBar.tsx6
-rw-r--r--src/fields/Doc.ts14
-rw-r--r--src/fields/SchemaHeaderField.ts92
-rw-r--r--src/fields/ScriptField.ts24
-rw-r--r--src/fields/util.ts9
41 files changed, 1274 insertions, 1048 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 99c04b673..c9424a92e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1727,7 +1727,8 @@ export namespace DocUtils {
.replace(/\.[a-z0-9]*$/, '');
if (Upload.isImageInformation(result)) {
const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim);
- proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (StrCast((result.exifData?.data as any)?.Orientation).includes('Rotate 90') ? 5 : undefined);
+ const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase();
+ proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
if (NumCast(proto['data-nativeOrientation']) >= 5) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f7d072d80..99a8c895f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -235,9 +235,9 @@ export class CurrentUserUtils {
const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text",
layout:
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
- ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
- " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' background='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' background='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
+ ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
+ " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
+ ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
"</HTMLdiv>"
}, "header");
@@ -267,7 +267,7 @@ export class CurrentUserUtils {
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
// {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
- {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
{key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree,
treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
@@ -633,7 +633,7 @@ export class CurrentUserUtils {
{ title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
{ title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
{ title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
- { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+ { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
{ title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
];
}
@@ -675,8 +675,9 @@ export class CurrentUserUtils {
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
CollectionViewType.Grid, CollectionViewType.NoteTaking]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
+ { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_)'}},
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement().currentFrame.toString()'}, width: 20, scripts: {}},
+ { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20, scripts: {}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
{ title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index d781a87ab..6386c87a0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -457,8 +457,7 @@ export namespace DragManager {
document.removeEventListener('pointerup', upHandler, true);
SnappingManager.SetIsDragging(false);
SnappingManager.clearSnapLines();
- const ended = batch.end();
- if (undo && ended) UndoManager.Undo();
+ if (batch.end() && undo) UndoManager.Undo();
docsBeingDragged.length = 0;
});
var startWindowDragTimer: any;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 256ab5c44..7c209d1e0 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,37 +1,41 @@
-import { DragManager } from "./DragManager";
-import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { ObjectField } from "../../fields/ObjectField";
-import { StrCast, Cast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
-import { ScriptField, ComputedField } from "../../fields/ScriptField";
-import { RichTextField } from "../../fields/RichTextField";
-import { ImageField } from "../../fields/URLField";
-import { ScriptingGlobals } from "./ScriptingGlobals";
-import { listSpec } from "../../fields/Schema";
+import { DragManager } from './DragManager';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { ObjectField } from '../../fields/ObjectField';
+import { StrCast, Cast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
+import { ScriptField, ComputedField } from '../../fields/ScriptField';
+import { RichTextField } from '../../fields/RichTextField';
+import { ImageField } from '../../fields/URLField';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { listSpec } from '../../fields/Schema';
+import { ButtonType } from '../views/nodes/button/FontIconBox';
-export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = "") {
+export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') {
if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated
doc.isTemplateDoc = makeTemplate(doc, first, rename);
return doc;
}
-//
+//
// converts 'doc' into a template that can be used to render other documents.
// the title of doc is used to determine which field is being templated, so
-// passing a value for 'rename' allows the doc to be given a meangingful name
+// passing a value for 'rename' allows the doc to be given a meangingful name
// after it has been converted to
function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
- if (layoutDoc.layout instanceof Doc) { // its already a template
+ if (layoutDoc.layout instanceof Doc) {
+ // its already a template
return true;
}
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
- const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
+ const fieldKey = layout.replace("fieldKey={'", '').replace(/'}$/, '');
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
docs.forEach(d => {
- if (!StrCast(d.title).startsWith("-")) {
- const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "");
+ if (!StrCast(d.title).startsWith('-')) {
+ const params = StrCast(d.title)
+ .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1]
+ .replace('()', '');
if (params) {
any = makeTemplate(d, false) || any;
d.PARAMS = params;
@@ -43,12 +47,13 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
}
});
if (first) {
- if (!docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template
+ if (!docs.length) {
+ // bcz: feels hacky : if the root level document has items, it's not a field template
any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any;
}
}
if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
- if (!StrCast(layoutDoc.title).startsWith("-")) {
+ if (!StrCast(layoutDoc.title).startsWith('-')) {
any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
}
}
@@ -59,23 +64,29 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data?.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes("FontIconBox")) {
+ if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) {
if (data.removeDropProperties || dbox.removeDropProperties) {
//dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon
dbox = Doc.MakeAlias(dbox);
- const dragProps = Cast(dbox.removeDropProperties, listSpec("string"), []);
+ const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
- remProps.map(prop => dbox[prop] = undefined);
+ remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
- const layoutDoc = doc;// doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type !== DocumentType.FONTICON) {
!layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
}
layoutDoc.isTemplateDoc = true;
dbox = Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
- backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt"
+ _nativeWidth: 100,
+ _nativeHeight: 100,
+ _width: 100,
+ _height: 100,
+ backgroundColor: StrCast(doc.backgroundColor),
+ title: StrCast(layoutDoc.title),
+ btnType: ButtonType.ClickButton,
+ icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt',
});
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
@@ -86,5 +97,10 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data.droppedDocuments[i] = dbox;
});
}
-ScriptingGlobals.add(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); },
- "converts the dropped data to buttons", "(dragData: any)"); \ No newline at end of file
+ScriptingGlobals.add(
+ function convertToButtons(dragData: any) {
+ convertDropDataToButtons(dragData as DragManager.DocumentDragData);
+ },
+ 'converts the dropped data to buttons',
+ '(dragData: any)'
+);
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 289c5bc51..4af51b9a0 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,12 +1,12 @@
-import React = require("react");
-import { Utils } from "../../Utils";
-import "./InteractionUtils.scss";
+import React = require('react');
+import { Utils } from '../../Utils';
+import './InteractionUtils.scss';
export namespace InteractionUtils {
- export const MOUSETYPE = "mouse";
- export const TOUCHTYPE = "touch";
- export const PENTYPE = "pen";
- export const ERASERTYPE = "eraser";
+ export const MOUSETYPE = 'mouse';
+ export const TOUCHTYPE = 'touch';
+ export const PENTYPE = 'pen';
+ export const ERASERTYPE = 'eraser';
const POINTER_PEN_BUTTON = -1;
const REACT_POINTER_PEN_BUTTON = 0;
@@ -19,24 +19,23 @@ export namespace InteractionUtils {
readonly touches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly changedTouches: T extends React.TouchEvent ? React.Touch[] : Touch[],
readonly touchEvent: T extends React.TouchEvent ? React.TouchEvent : TouchEvent
- ) { }
+ ) {}
}
- export interface MultiTouchEventDisposer { (): void; }
+ export interface MultiTouchEventDisposer {
+ (): void;
+ }
/**
*
* @param element - element to turn into a touch target
* @param startFunc - event handler, typically Touchable.onTouchStart (classes that inherit touchable can pass in this.onTouchStart)
*/
- export function MakeMultiTouchTarget(
- element: HTMLElement,
- startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeMultiTouchTarget(element: HTMLElement, startFunc: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const onMultiTouchStartHandler = (e: Event) => startFunc(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
// const onMultiTouchMoveHandler = moveFunc ? (e: Event) => moveFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
// const onMultiTouchEndHandler = endFunc ? (e: Event) => endFunc(e, (e as CustomEvent<MultiTouchEvent<TouchEvent>>).detail) : undefined;
- element.addEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.addEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.addEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -44,7 +43,7 @@ export namespace InteractionUtils {
// element.addEventListener("dashOnTouchEnd", onMultiTouchEndHandler);
// }
return () => {
- element.removeEventListener("dashOnTouchStart", onMultiTouchStartHandler);
+ element.removeEventListener('dashOnTouchStart', onMultiTouchStartHandler);
// if (onMultiTouchMoveHandler) {
// element.removeEventListener("dashOnTouchMove", onMultiTouchMoveHandler);
// }
@@ -59,14 +58,11 @@ export namespace InteractionUtils {
* @param element - element to add events to
* @param func - function to add to the event
*/
- export function MakeHoldTouchTarget(
- element: HTMLElement,
- func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void
- ): MultiTouchEventDisposer {
+ export function MakeHoldTouchTarget(element: HTMLElement, func: (e: Event, me: MultiTouchEvent<React.TouchEvent>) => void): MultiTouchEventDisposer {
const handler = (e: Event) => func(e, (e as CustomEvent<MultiTouchEvent<React.TouchEvent>>).detail);
- element.addEventListener("dashOnTouchHoldStart", handler);
+ element.addEventListener('dashOnTouchHoldStart', handler);
return () => {
- element.removeEventListener("dashOnTouchHoldStart", handler);
+ element.removeEventListener('dashOnTouchHoldStart', handler);
};
}
@@ -89,71 +85,108 @@ export namespace InteractionUtils {
return myTouches;
}
- export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
- color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
- downHdlr?: ((e: React.PointerEvent) => void)) {
+ export function CreatePolyline(
+ points: { X: number; Y: number }[],
+ left: number,
+ top: number,
+ color: string,
+ width: number,
+ strokeWidth: number,
+ lineJoin: string,
+ lineCap: string,
+ bezier: string,
+ fill: string,
+ arrowStart: string,
+ arrowEnd: string,
+ markerScale: number,
+ dash: string | undefined,
+ scalex: number,
+ scaley: number,
+ shape: string,
+ pevents: string,
+ opacity: number,
+ nodefs: boolean,
+ downHdlr?: (e: React.PointerEvent) => void
+ ) {
const pts = shape ? makePolygon(shape, points) : points;
if (isNaN(scalex)) scalex = 1;
if (isNaN(scaley)) scaley = 1;
- const toScr = (p: { X: number, Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
- const strpts = bezier ?
- pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : (i === 0 ? "M" + toScr(pt) : "") + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") :
- pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, "");
+ const toScr = (p: { X: number; Y: number }) => ` ${!p ? 0 : (p.X - left - width / 2) * scalex + width / 2}, ${!p ? 0 : (p.Y - top - width / 2) * scaley + width / 2} `;
+ const strpts = bezier
+ ? pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? '' : (i === 0 ? 'M' + toScr(pt) : '') + 'C' + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), '')
+ : pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, '');
const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined;
const defGuid = Utils.GenerateGuid();
- const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
+ const Tag = (bezier ? 'path' : 'polyline') as keyof JSX.IntrinsicElements;
const markerStrokeWidth = strokeWidth / 2;
- const arrowWidthFactor = 3 * (markerScale || 0.5);// used to be 1.5
+ const arrowWidthFactor = 3 * (markerScale || 0.5); // used to be 1.5
const arrowLengthFactor = 5 * (markerScale || 0.5);
const arrowNotchFactor = 2 * (markerScale || 0.5);
- return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
- {nodefs ? (null) : <defs>
- {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
- <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
- <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
- </marker>}
- {arrowStart !== "arrow" ? (null) :
- <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${markerStrokeWidth * arrowWidthFactor}, 0 0`} />
- </marker>}
- {arrowEnd !== "arrow" ? (null) :
- <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={markerStrokeWidth * 2 / 3}
- points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} />
- </marker>}
- </defs>}
-
- <Tag
- d={bezier ? strpts : undefined}
- points={bezier ? undefined : strpts}
- style={{
- // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
- fill: fill && fill !== "transparent" ? fill : "none",
- opacity: 1.0,
- // opacity: strokeWidth !== width ? 0.5 : undefined,
- pointerEvents: pevents as any,
- stroke: color ?? "rgb(0, 0, 0)",
- strokeWidth: strokeWidth,
- strokeLinecap: lineCap as any,
- strokeDasharray: dashArray
- }}
- markerStart={`url(#${arrowStart === "dot" ? arrowStart + defGuid : arrowStart + "Start" + defGuid})`}
- markerEnd={`url(#${arrowEnd === "dot" ? arrowEnd + defGuid : arrowEnd + "End" + defGuid})`}
- />
-
- </svg>);
+ return (
+ <svg fill={color} style={{ transition: 'inherit' }} onPointerDown={downHdlr}>
+ {' '}
+ {/* setting the svg fill sets the arrowStart fill */}
+ {nodefs ? null : (
+ <defs>
+ {arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
+ <marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
+ <circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
+ </marker>
+ )}
+ {arrowStart !== 'arrow' ? null : (
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`${arrowLengthFactor * markerStrokeWidth} ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * markerStrokeWidth} ${
+ markerStrokeWidth * arrowWidthFactor
+ }, 0 0`}
+ />
+ </marker>
+ )}
+ {arrowEnd !== 'arrow' ? null : (
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={markerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+ <polygon
+ style={{ stroke: color }}
+ strokeLinejoin={lineJoin as any}
+ strokeWidth={(markerStrokeWidth * 2) / 3}
+ points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
+ />
+ </marker>
+ )}
+ </defs>
+ )}
+ <Tag
+ d={bezier ? strpts : undefined}
+ points={bezier ? undefined : strpts}
+ style={{
+ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
+ fill: fill && fill !== 'transparent' ? fill : 'none',
+ opacity: 1.0,
+ // opacity: strokeWidth !== width ? 0.5 : undefined,
+ pointerEvents: pevents as any,
+ stroke: color ?? 'rgb(0, 0, 0)',
+ strokeWidth: strokeWidth,
+ strokeLinecap: lineCap as any,
+ strokeDasharray: dashArray,
+ transition: 'inherit',
+ }}
+ markerStart={`url(#${arrowStart === 'dot' ? arrowStart + defGuid : arrowStart + 'Start' + defGuid})`}
+ markerEnd={`url(#${arrowEnd === 'dot' ? arrowEnd + defGuid : arrowEnd + 'End' + defGuid})`}
+ />
+ </svg>
+ );
}
- export function makePolygon(shape: string, points: { X: number, Y: number }[]) {
+ export function makePolygon(shape: string, points: { X: number; Y: number }[]) {
if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y + 1 === points[0].Y) {
//pointer is up (first and last points are the same)
- if (shape === "arrow" || shape === "line" || shape === "circle") {
+ if (shape === 'arrow' || shape === 'line' || shape === 'circle') {
//if arrow or line, the two end points should be the starting and the ending point
var left = points[0].X;
var top = points[0].Y;
@@ -175,7 +208,7 @@ export namespace InteractionUtils {
left = points[0].X;
bottom = points[points.length - 1].Y;
top = points[0].Y;
- if (shape !== "arrow" && shape !== "line" && shape !== "circle") {
+ if (shape !== 'arrow' && shape !== 'line' && shape !== 'circle') {
//switch left/right and top/bottom if needed
if (left > right) {
const temp = right;
@@ -191,14 +224,13 @@ export namespace InteractionUtils {
}
points = [];
switch (shape) {
- case "rectangle":
+ case 'rectangle':
points.push({ X: left, Y: top });
points.push({ X: right, Y: top });
points.push({ X: right, Y: bottom });
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: top });
- return points;
- case "triangle":
+ case 'triangle':
// points.push({ X: left, Y: bottom });
// points.push({ X: right, Y: bottom });
// points.push({ X: (right + left) / 2, Y: top });
@@ -219,62 +251,39 @@ export namespace InteractionUtils {
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
-
-
- return points;
- case "circle":
+ case 'circle':
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
points.push({ X: x, Y: y });
}
for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - (Math.pow((y - centerY), 2))) + centerX;
+ const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
const newX = centerX - (x - centerX);
points.push({ X: newX, Y: y });
}
- points.push({ X: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(top, bottom) - centerY), 2))) + centerX, Y: Math.min(top, bottom) });
+ points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) });
} else {
for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
points.push({ X: x, Y: y });
}
for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - (Math.pow((x - centerX), 2))) + centerY;
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
const newY = centerY - (y - centerY);
points.push({ X: x, Y: newY });
}
- points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - (Math.pow((Math.min(left, right) - centerX), 2))) + centerY });
+ points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
}
- return points;
- // case "arrow":
- // const x1 = left;
- // const y1 = top;
- // const x2 = right;
- // const y2 = bottom;
- // const L1 = Math.sqrt(Math.pow(Math.abs(x1 - x2), 2) + (Math.pow(Math.abs(y1 - y2), 2)));
- // const L2 = L1 / 5;
- // const angle = 0.785398;
- // const x3 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) + (y1 - y2) * Math.sin(angle));
- // const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
- // const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
- // const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- // points.push({ X: x1, Y: y1 });
- // points.push({ X: x2, Y: y2 });
- // points.push({ X: x3, Y: y3 });
- // points.push({ X: x4, Y: y4 });
- // points.push({ X: x2, Y: y2 });
- // return points;
- case "line":
+ case 'line':
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
return points;
- default:
- return points;
}
+ return points;
}
/**
* Returns whether or not the pointer event passed in is of the type passed in
@@ -284,11 +293,14 @@ export namespace InteractionUtils {
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
- case PENTYPE: return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
- case ERASERTYPE: return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
+ case PENTYPE:
+ return e.pointerType === PENTYPE && (e.button === -1 || e.button === 0);
+ case ERASERTYPE:
+ return e.pointerType === PENTYPE && e.button === (e instanceof PointerEvent ? ERASER_BUTTON : ERASER_BUTTON);
case TOUCHTYPE:
return e.pointerType === TOUCHTYPE;
- default: return e.pointerType === type;
+ default:
+ return e.pointerType === type;
}
}
@@ -305,7 +317,7 @@ export namespace InteractionUtils {
* Returns the centroid of an n-arbitrary long list of points (takes the average the x and y components of each point)
* @param pts - n-arbitrary long list of points
*/
- export function CenterPoint(pts: React.Touch[]): { X: number, Y: number } {
+ export function CenterPoint(pts: React.Touch[]): { X: number; Y: number } {
const centerX = pts.map(pt => pt.clientX).reduce((a, b) => a + b, 0) / pts.length;
const centerY = pts.map(pt => pt.clientY).reduce((a, b) => a + b, 0) / pts.length;
return { X: centerX, Y: centerY };
@@ -324,9 +336,9 @@ export namespace InteractionUtils {
const newDist = TwoPointEuclidist(pt1, pt2);
/** if they have the same sign, then we are either pinching in or out.
- * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
- * so that it can still pan without freaking out
- */
+ * threshold it by 10 (it has to be pinching by at least threshold to be a valid pinch)
+ * so that it can still pan without freaking out
+ */
if (Math.sign(oldDist) === Math.sign(newDist) && Math.abs(oldDist - newDist) > threshold) {
return Math.sign(oldDist - newDist);
}
@@ -372,8 +384,6 @@ export namespace InteractionUtils {
// These might not be very useful anymore, but I'll leave them here for now -syip2
{
-
-
/**
* Returns the type of Touch Interaction from a list of points.
* Also returns any data that is associated with a Touch Interaction
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
new file mode 100644
index 000000000..5a2f2fcad
--- /dev/null
+++ b/src/client/util/ReportManager.scss
@@ -0,0 +1,88 @@
+@import '../views/global/globalCssVariables';
+
+.issue-list-wrapper {
+ position: relative;
+ min-width: 250px;
+ background-color: $light-blue;
+ overflow-y: scroll;
+}
+
+.issue-list {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 5px;
+ margin: 5px;
+ border-radius: 5px;
+ border: 1px solid grey;
+ background-color: lightgoldenrodyellow;
+}
+
+// issue should pop up when the user hover over the issue
+.issue-list:hover {
+ box-shadow: 2px;
+ cursor: pointer;
+ border: 3px solid #252b33;
+}
+
+.issue-content {
+ background-color: white;
+ padding: 10px;
+ flex: 1 1 auto;
+ overflow-y: scroll;
+}
+
+.issue-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: black;
+}
+
+.issue-body {
+ padding: 0 10px;
+ width: 100%;
+ text-align: left;
+}
+
+.issue-body > * {
+ margin-top: 5px;
+}
+
+.issue-body img,
+.issue-body video {
+ display: block;
+ max-width: 100%;
+}
+
+.report-issue-fab {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.loading-center {
+ margin: auto 0;
+}
+
+.settings-content label {
+ margin-top: 10px;
+}
+
+.report-disclaimer {
+ font-size: 8px;
+ color: grey;
+ padding-right: 50px;
+ font-style: italic;
+ text-align: left;
+}
+
+.flex-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
new file mode 100644
index 000000000..55c5ca87f
--- /dev/null
+++ b/src/client/util/ReportManager.tsx
@@ -0,0 +1,282 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { ColorState, SketchPicker } from 'react-color';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
+import { DocServer } from '../DocServer';
+import { Networking } from '../Network';
+import { MainViewModal } from '../views/MainViewModal';
+import { FontIconBox } from '../views/nodes/button/FontIconBox';
+import { DragManager } from './DragManager';
+import { GroupManager } from './GroupManager';
+import './SettingsManager.scss';
+import './ReportManager.scss';
+import { undoBatch } from './UndoManager';
+import { Octokit } from "@octokit/core";
+import { CheckBox } from '../views/search/CheckBox';
+import ReactLoading from 'react-loading';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+const higflyout = require('@hig/flyout');
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ private octokit: Octokit;
+
+ @observable public issues: any[] = [];
+ @action setIssues = action((issues: any[]) => { this.issues = issues; });
+
+ // undefined is the default - null is if the user is making an issue
+ @observable public selectedIssue: any = undefined;
+ @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
+
+ // only get the open issues
+ @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
+
+ public updateIssueSearch = action((query: string = '') => {
+ if (query === '') {
+ this.shownIssues = this.issues.filter(issue => issue.state === 'open');
+ return;
+ }
+ this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ this.octokit = new Octokit({
+ auth: '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'
+ });
+ }
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(() => {
+ if (this.issues.length === 0) {
+ // load in the issues if not already loaded
+ this.getAllIssues()
+ .then(issues => {
+ this.setIssues(issues);
+ this.updateIssueSearch();
+ })
+ .catch(err => console.log(err));
+ }
+ (this.isOpen = true)
+ });
+
+ @observable private bugTitle = '';
+ @action setBugTitle = action((title: string) => { this.bugTitle = title; });
+ @observable private bugDescription = '';
+ @action setBugDescription = action((description: string) => { this.bugDescription = description; });
+ @observable private bugType = '';
+ @action setBugType = action((type: string) => { this.bugType = type; });
+ @observable private bugPriority = '';
+ @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
+
+ // private toGithub = false;
+ // will always be set to true - no alterntive option yet
+ private toGithub = true;
+
+ private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+ public async getAllIssues() : Promise<any[]> {
+ const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+ }
+
+ public async reportIssue() {
+ if (this.bugTitle === '' || this.bugDescription === ''
+ || this.bugType === '' || this.bugPriority === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+
+
+ if (this.toGithub) {
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
+ body: `${this.bugDescription} \n\nfiles:\n${(this.fileLinks ?? []).join('\n')}`,
+ labels: [
+ 'from-dash-app',
+ this.bugType,
+ this.bugPriority
+ ]
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ // on error, don't close the modal
+ return;
+ }
+ }
+ else {
+ // if not going to github issues, not sure what to do yet...
+ }
+
+ // if we're down here, then we're good to go. reset the fields.
+ this.setBugTitle('');
+ this.setBugDescription('');
+ this.toGithub = false;
+ this.setFileLinks([]);
+ this.setBugType('');
+ this.setBugPriority('');
+ this.close();
+ }
+
+ @observable public fileLinks: any = [];
+ @action setFileLinks = action((links: any) => { this.fileLinks = links; });
+
+ private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
+
+ private uploadFiles = (input: any) => {
+ // keep null while uploading
+ this.setFileLinks(null);
+ // upload the files to the server
+ if (input.files && input.files.length !== 0) {
+ const fileArray: File[] = Array.from(input.files);
+ (Networking.UploadFilesToServer(fileArray)).then(links => {
+ console.log('finshed uploading', links.map(this.getServerPath));
+ this.setFileLinks((links ?? []).map(this.getServerPath));
+ })
+ }
+
+ }
+
+
+ private renderIssue = (issue: any) => {
+
+ const isReportingIssue = issue === null;
+
+ return isReportingIssue ?
+ // report issue
+ (<div className="settings-content">
+ <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
+ <label>Please leave a title for the bug.</label><br />
+ <input type="text" placeholder='title' onChange={(e) => this.bugTitle = e.target.value} required/>
+ <br />
+ <label>Please leave a description for the bug and how it can be recreated.</label>
+ <textarea placeholder='description' onChange={(e) => this.bugDescription = e.target.value} required/>
+ <br />
+ {/* {<label>Send to github issues? </label>
+ <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
+ <br /> } */}
+
+ <label>Please label the issue</label>
+ <div className='flex-select'>
+ <select name="bugType">
+ <option value="" disabled selected>Type</option>
+ <option value="bug">Bug</option>
+ <option value="cosmetic">Poor Design or Cosmetic</option>
+ <option value="documentation">Poor Documentation</option>
+ </select>
+
+ <select name="bigPriority">
+ <option value="" disabled selected>Priority</option>
+ <option value="priority-low">Low</option>
+ <option value="priority-medium">Medium</option>
+ <option value="priority-high">High</option>
+ </select>
+ </div>
+
+
+ <div>
+ <label>Upload media that shows the bug (optional)</label>
+ <input type="file" name="file" multiple accept='audio/*, video/*' onChange={e => this.uploadFiles(e.target)}/>
+ </div>
+ <br />
+
+ <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
+ </div>)
+ :
+ // view issue
+ (
+ <div className='issue-container'>
+ <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
+ <div className='issue-title'>
+ {issue.title}
+ </div>
+ <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+ }
+
+ private showReportIssueScreen = () => {
+ this.setSelectedIssue(null);
+ }
+
+ private closeReportIssueScreen = () => {
+ this.setSelectedIssue(undefined);
+ }
+
+ private get reportInterface() {
+
+ const isReportingIssue = this.selectedIssue === null;
+
+ return (
+ <div className="settings-interface">
+ <div className='issue-list-wrapper'>
+ <h3>Current Issues</h3>
+ <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
+ {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
+
+ {/* <div className="settings-user">
+ <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
+ </div> */}
+ </div>
+
+ <div className="close-button" onClick={this.close}>
+ <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ </div>
+
+ <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
+ {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
+ </div>
+
+ <div className='report-issue-fab'>
+ <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
+ <button
+ onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
+ >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
+ </div>
+
+
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ />
+ );
+ }
+}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1c84af94a..7a555d5f8 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -101,6 +101,9 @@ export namespace SelectionManager {
}
}
ScriptingGlobals.add(function SelectionManager_selectedDocType(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
+ if (colType === ('tab' as any)) {
+ return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
+ }
let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
});
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 280ca8a8c..886dd974b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -132,20 +132,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
- styleFromLayoutString = (scale: number) => {
- const style: { [key: string]: any } = {};
- const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'background', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
- const replacer = (match: any, expr: string, offset: any, string: any) => {
- // bcz: this executes a script to convert a property expression string: { script } into a value
- return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result?.toString() ?? '';
- };
- divKeys.map((prop: string) => {
- const p = (this.props as any)[prop];
- typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
- });
- return style;
- };
-
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@computed public get annotationKey() {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6d1397395..3589e014a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -203,8 +203,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (this._deleteAfterIconify) {
views.forEach(iconView => {
Doc.setNativeView(iconView.props.Document);
- if (iconView.props.Document.isInkMask && iconView.props.Document.activeFrame !== undefined) {
- iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMaks to be "turned off" without removing them from the collection which allows them to function properly in a presenation.
+ if (iconView.props.Document.activeFrame) {
+ iconView.props.Document.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
} else {
iconView.props.removeDocument?.(iconView.props.Document);
}
@@ -358,8 +358,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerDown = (e: React.PointerEvent): void => {
- DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc));
- this._inkDragDocs = DragManager.docsBeingDragged
+ const views = SelectionManager.Views().map(dv => dv.rootDoc);
+ this._inkDragDocs = views
.filter(doc => doc.type === DocumentType.INK)
.map(doc => {
if (InkStrokeProperties.Instance._lock) {
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 664f2448b..bed7caf7f 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -1,22 +1,23 @@
.inkstroke-UI {
- // transform-origin: top left;
- position: absolute;
- overflow: visible;
- pointer-events: none;
- z-index: 2001; // 1 higher than documentdecorations
-
- svg:not(:root) {
- overflow: visible !important;
+ // transform-origin: top left;
position: absolute;
- left:0;
- top:0;
- }
+ overflow: visible;
+ pointer-events: none;
+ z-index: 2001; // 1 higher than documentdecorations
+
+ svg:not(:root) {
+ overflow: visible !important;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
}
.inkStroke-wrapper {
display: flex;
align-items: center;
height: 100%;
+ transition: inherit;
.inkStroke {
mix-blend-mode: multiply;
stroke-linejoin: round;
@@ -26,8 +27,10 @@
width: 100%;
height: 100%;
pointer-events: none;
+ transition: inherit;
svg:not(:root) {
overflow: visible !important;
+ transition: inherit;
}
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 821e2f739..d19a916f9 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -347,7 +347,6 @@ export class InkStrokeProperties {
const deltaX = snapData.nearestPt.X - ink[controlIndex].X;
const deltaY = snapData.nearestPt.Y - ink[controlIndex].Y;
const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
- console.log('X = ' + snapData.nearestPt.X + ' ' + snapData.nearestPt.Y);
return res;
}
}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 2671aea56..dae1c10bb 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,7 +25,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
@@ -45,6 +45,10 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
import './InkStroke.scss';
import Color = require('color');
+import { ComputedField } from '../../fields/ScriptField';
+import { listSpec } from '../../fields/Schema';
+import { List } from '../../fields/List';
+import { StyleProp } from './StyleProvider';
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -56,7 +60,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
}
private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
- private _selDisposer?: IReactionDisposer;
+ private _disposers: { [key: string]: IReactionDisposer } = {};
@observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
@observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
@@ -64,13 +68,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
componentDidMount() {
this.props.setContentView?.(this);
- this._selDisposer = reaction(
+ this._disposers.selfDisper = reaction(
() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
selected => !selected && (InkStrokeProperties.Instance._controlButton = false)
);
}
componentWillUnmount() {
- this._selDisposer?.();
+ Object.keys(this._disposers).forEach(key => this._disposers[key]());
}
// transform is the inherited screentolocal xf plus any scaling that was done to make the stroke
@@ -356,7 +360,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
const closed = InkingStroke.IsClosed(inkData);
const isInkMask = BoolCast(this.layoutDoc.isInkMask);
const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent');
- const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : StrCast(this.layoutDoc.color);
+ const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
// bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) {
@@ -401,13 +405,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
inkTop,
highlightColor,
inkStrokeWidth,
- fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0),
+ Math.max(5, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0)),
StrCast(this.layoutDoc.strokeLineJoin),
StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier),
!closed ? 'none' : fillColor === 'transparent' || suppressFill ? 'none' : fillColor,
- startMarker,
- endMarker,
+ '',
+ '',
markerScale,
undefined,
inkScaleX,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c166594e5..06be4d194 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -20,6 +20,7 @@ import { DocumentManager } from '../util/DocumentManager';
import { GroupManager } from '../util/GroupManager';
import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
+import { ReportManager } from '../util/ReportManager';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SelectionManager } from '../util/SelectionManager';
import { ColorScheme, SettingsManager } from '../util/SettingsManager';
@@ -171,6 +172,7 @@ export class MainView extends React.Component {
'curPage',
'viewType',
'chromeHidden',
+ 'currentFrame',
'width',
'nativeWidth',
]); // can play with these fields on someone else's
@@ -975,10 +977,11 @@ export class MainView extends React.Component {
<DictationOverlay />
<SharingManager />
<SettingsManager />
+ <ReportManager />
<CaptureManager />
<GroupManager />
<GoogleAuthenticationManager />
- <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfHeaderBarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
+ <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfSidebarDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} />
<ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
{this._hideUI ? null : <TopBar />}
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
@@ -991,7 +994,7 @@ export class MainView extends React.Component {
default:
return (
<>
- <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 1999 }}>
+ <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
<CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
</div>
{this.mainDashboardArea}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3bd4f5152..c0ba170c6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -40,7 +40,8 @@ export enum StyleProp {
JitterRotation = 'jitterRotation', // whether documents should be randomly rotated
BorderPath = 'customBorder', // border path for document view
FontSize = 'fontSize', // size of text font
- FontFamily = 'fontFamily', // size of text font
+ FontFamily = 'fontFamily', // font family of text
+ FontWeight = 'fontWeight', // font weight of text
}
function darkScheme() {
@@ -117,9 +118,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.HideLinkButton:
return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
case StyleProp.FontSize:
- return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
+ return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize)));
case StyleProp.FontFamily:
- return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
+ return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily)));
+ case StyleProp.FontWeight:
+ return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight)));
case StyleProp.ShowTitle:
return (
(doc &&
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index 17eff022f..b1cce8705 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -44,7 +44,8 @@ div .lm_header {
position: absolute;
width: calc(100% - 60px);
overflow: scroll;
- background: $dark-gray;
+ background: #6b6b6b6b; //$dark-gray;
+ border-radius: 5px;
}
.lm_tab {
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index eb55650e4..0dc30e0fd 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -587,49 +587,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
}
@undoBatch
- @action
- pinWithView = (targetDoc: Opt<Doc>) => {
- if (targetDoc) {
- TabDocView.PinDoc(targetDoc);
- const presArray: Doc[] = PresBox.Instance?.sortArray();
- const size: number = PresBox.Instance?._selectedArray.size;
- const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
- const activeDoc = presSelected ? PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.indexOf(presSelected) + 1] : PresBox.Instance?.childDocs[PresBox.Instance?.childDocs.length - 1];
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking || targetDoc._viewType === CollectionViewType.NoteTaking) {
- const scroll = targetDoc._scrollTop;
- activeDoc.presPinView = true;
- activeDoc.presPinViewScroll = scroll;
- } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.MAP) {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeDoc.presPinView = true;
- activeDoc.presPinViewX = x;
- activeDoc.presPinViewY = y;
- activeDoc.presPinViewScale = scale;
- } else if (targetDoc.type === DocumentType.VID) {
- activeDoc.presPinView = true;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const width = targetDoc._clipWidth;
- activeDoc.presPinClipWidth = width;
- activeDoc.presPinView = true;
- }
- }
- };
-
- @computed
- get pinWithViewButton() {
- const presPinWithViewIcon = <img src={`/assets/pinWithView.png`} style={{ margin: 'auto', width: 19 }} />;
- return !this.selectedDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Pin with current view'}</div>} placement="top">
- <button className="antimodeMenu-button" style={{ justifyContent: 'center' }} onClick={() => this.pinWithView(this.selectedDoc)}>
- {presPinWithViewIcon}
- </button>
- </Tooltip>
- );
- }
-
- @undoBatch
onAlias = () => {
if (this.selectedDoc && this.selectedDocumentView) {
// const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true);
@@ -722,7 +679,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
{this.aliasButton}
{/* {this.pinButton} */}
{this.toggleOverlayButton}
- {this.pinWithViewButton}
<div className="collectionMenu-divider" key="divider2"></div>
{this.subChrome}
<div className="collectionMenu-divider" key="divider3"></div>
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 5c8b10ae1..b359ef420 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -1,6 +1,6 @@
import React = require('react');
import { CursorProperty } from 'csstype';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
@@ -37,84 +37,82 @@ export type collectionNoteTakingViewProps = {
NativeHeight?: () => number;
};
-//TODO: somehow need to update the mapping and then have everything else rerender. Maybe with a refresh boolean like
-// in Hypermedia?
-
+/**
+ * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1)
+ * add and remove columns (2) change column sizes and (3) move documents within and between columns. This
+ * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is
+ * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in
+ * the rest of Dash, so it may be worthwhile to transition the headers to simple documents.
+ */
@observer
export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() {
_disposers: { [key: string]: IReactionDisposer } = {};
_masonryGridRef: HTMLDivElement | null = null;
- _draggerRef = React.createRef<HTMLDivElement>(); // change to relative widths for deleting. change storage from columnStartXCoords to columnHeaders (schemaHeaderFields has a widgth alrady)
- @observable columnStartXCoords: number[] = []; // columnHeaders -- SchemaHeaderField -- widht
+ _draggerRef = React.createRef<HTMLDivElement>();
@observable docsDraggedRowCol: number[] = [];
@observable _cursor: CursorProperty = 'grab';
- @observable _scroll = 0; // used to force the document decoration to update when scrolling
+ @observable _scroll = 0;
@computed get chromeHidden() {
return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
}
+ // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
@computed get columnHeaders() {
- const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null));
+ const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset'));
-
- // @#Oberable draggedColIndex = ...
- //@observable cloneDivXYcoords
- // @observable clonedDiv...
-
- // render() {
- // { this.clonedDiv ? <div clone styule={{transform: clonedDivXYCoords}} : null}
- // }
-
- // in NoteatakinView Column code, add a poinerDown handler that calls setupMoveUpEvents() which will clone the column div
- // and re-render it under the cursor during move events.
- // that move move event will update 2 observales -- the draggedColIndex up above, and the location of the clonedDiv so that the render in this view will know where to render the cloned div
- // add observable variable that tells drag column to rnder in a different location than where the schemaHeaderFiel sa y ot.
- // if (col 1 is where col 3) {
- // return 3 2 1 4 56
- // }
if (needsUnsetCategory) {
- columnHeaders.push(new SchemaHeaderField('unset'));
+ setTimeout(() => columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)));
}
return columnHeaders;
}
+ // notetakingCategoryField returns the key to accessing a document's column value
@computed get notetakingCategoryField() {
return 'NotetakingCategory';
}
- @computed get filteredChildren() {
- return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
- }
@computed get headerMargin() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
}
@computed get xMargin() {
return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth()));
}
+ // dividerWidth returns the width of a CollectionNoteTakingViewDivider
+ @computed get dividerWidth() {
+ return 32;
+ }
@computed get yMargin() {
return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5);
- } // 2 * this.gridGap)); }
+ }
@computed get gridGap() {
return NumCast(this.layoutDoc._gridGap, 10);
}
+ // numGroupColumns returns the number of columns
@computed get numGroupColumns() {
return this.columnHeaders.length;
}
+ // PanelWidth returns the size of the total available space the view occupies
@computed get PanelWidth() {
return this.props.PanelWidth();
}
- @computed get maxColWdith() {
+ // maxColWidth returns the maximum column width, which is slightly less than the total available space.
+ @computed get maxColWidth() {
return this.props.PanelWidth() - 2 * this.xMargin;
}
+ // availableWidth is the total amount of non-divider width. Since widths are stored relatively,
+ // we use availableWidth to convert from a percentage to a pixel count.
+ @computed get availableWidth() {
+ const numDividers = this.columnHeaders.length - 1;
+ return this.maxColWidth - numDividers * this.dividerWidth;
+ }
- // If the user has not yet created any docs (in another view), this will create a single column. Otherwise,
- // it will adjust according to the
+ // Documents should NOT have column category fields until entering this view, so the contructor creates the 'New Column'
+ // category for the user to then edit later.
constructor(props: any) {
super(props);
if (this.columnHeaders === undefined) {
- this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column')]);
- // add all of the docs that have not been added to a column to this new column
+ this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column', undefined, undefined, 1)]);
}
}
- // passed as a prop to the NoteTakingField, which uses this function
+ // children is passed as a prop to the NoteTakingField, which uses this function
// to render the docs you see within an individual column.
children = (docs: Doc[]) => {
TraceMobx();
@@ -130,39 +128,30 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
});
};
+ // Sections is one of the more important functions in this file, rendering the the documents
+ // for the UI. It properly renders documents being dragged between columns.
// [CAVEATS] (1) keep track of the offsetting
// (2) documentView gets unmounted as you remove it from the list
@computed get Sections() {
TraceMobx();
const columnHeaders = this.columnHeaders;
- let docs = this.childDocs;
+ // filter out the currently dragged docs from the child docs, since we will insert them later
+ const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d));
const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
const rowCol = this.docsDraggedRowCol;
-
- // filter out the currently dragged docs from the child docs, since we will insert them later
- if (rowCol.length && DragManager.docsBeingDragged.length) {
- const docIdsToRemove = new Set();
- DragManager.docsBeingDragged.forEach(d => docIdsToRemove.add(d[Id]));
- docs = docs.filter(d => !docIdsToRemove.has(d[Id]));
- }
-
// this will sort the docs into the correct columns (minus the ones you're currently dragging)
docs.map(d => {
const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
-
// look for if header exists already
const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
if (existingHeader) {
sections.get(existingHeader)!.push(d);
}
});
-
// now we add back in the docs that we're dragging
- if (rowCol.length && DragManager.docsBeingDragged.length) {
- const colHeader = columnHeaders[rowCol[1]];
- // TODO: get the actual offset that occurs if the docs were in that column
+ if (rowCol.length) {
const offset = 0;
- sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
+ sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
}
return sections;
}
@@ -173,6 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
100
);
};
+
componentDidMount() {
super.componentDidMount?.();
document.addEventListener('pointerup', this.removeDocDragHighlight, true);
@@ -180,11 +170,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
() => this.layoutDoc._autoHeight,
autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))))
);
- this._disposers.headers = reaction(
- () => this.columnHeaders.slice(),
- headers => this.resizeColumns(headers.length),
- { fireImmediately: true }
- );
}
componentWillUnmount() {
@@ -206,6 +191,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
@computed get onChildClickHandler() {
return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
}
+
@computed get onChildDoubleClickHandler() {
return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
}
@@ -225,7 +211,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
// let's dive in and get the actual document we want to drag/move around
focusDocument = (doc: Doc, options?: DocFocusOptions) => {
Doc.BrushDoc(doc);
-
let focusSpeed = 0;
const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
if (found) {
@@ -260,7 +245,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- // rules for displaying the documents
+ // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView)
getDisplayDoc(doc: Doc, width: () => number) {
const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
@@ -316,7 +301,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
);
}
- // This is used to get the coordinates of a document when we go from a view like freeform to columns
+ // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns
getDocTransform(doc: Doc, dref?: DocumentView) {
const y = this._scroll; // required for document decorations to update when the text box container is scrolled
const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined);
@@ -329,9 +314,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
getDocWidth(d: Doc) {
const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
- const index = existingHeader ? this.columnHeaders.indexOf(existingHeader) : 0;
- const endColValue = index === this.columnHeaders.length - 1 || index > this.columnStartXCoords.length - 1 ? this.PanelWidth : this.columnStartXCoords[index + 1];
- const maxWidth = index > this.columnStartXCoords.length - 1 ? this.PanelWidth : endColValue - this.columnStartXCoords[index] - 3 * this.xMargin;
+ const existingWidth = existingHeader?.width ? existingHeader.width : 0;
+ const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth - 2 * this.xMargin : this.maxColWidth - 2 * this.xMargin;
if (d.type === DocumentType.RTF) {
return maxWidth;
}
@@ -358,28 +342,36 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
return Math.min(childHeight, maxHeight, panelHeight);
}
- // called when a column is either added or deleted. This function creates n evenly spaced columns
+ // resizeColumns is called whenever a user adds or removes a column. When removing,
+ // this function renormalizes the column widths to fill the newly available space
+ // in the panel. When adding, this function renormalizes the existing columns to take up
+ // (n - 1)/n space, since the new column will be allocated 1/n of the total space.
+ // Column widths are relative (portion of available space) and stored in the 'width'
+ // field of SchemaHeaderFields.
+ //
+ // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375].
+ // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33]
@action
- resizeColumns = (n: number) => {
- const totalWidth = this.PanelWidth;
- const dividerWidth = 32;
- const totaldividerWidth = (n - 1) * dividerWidth;
- const colWidth = (totalWidth - totaldividerWidth) / n;
- const newColXCoords: number[] = [];
- let colStart = 0;
- for (let i = 0; i < n; i++) {
- newColXCoords.push(colStart);
- colStart += colWidth + dividerWidth;
+ resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => {
+ const n = this.columnHeaders.length;
+ if (n == 1) {
+ this.columnHeaders[0].setWidth(1);
+ return true;
}
- this.columnStartXCoords = newColXCoords;
+ const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth);
+ this.columnHeaders.forEach((h, i) => {
+ if (!(isAdd && i == colIndex)) {
+ h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor);
+ }
+ });
+ return true;
};
- // This function is used to preview where a document will drop in a column once a drag is complete.
+ // onPointerMove is used to preview where a document will drop in a column once a drag is complete.
@action
onPointerMove = (force: boolean, ex: number, ey: number) => {
if (this.childDocList && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) {
// get the current docs for the column based on the mouse's x coordinate
- // will use again later, which is why we're saving as local
const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap;
const colDocs = this.getDocsFromXCoord(xCoord);
// get the index for where you need to insert the doc you are currently dragging
@@ -408,10 +400,16 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
};
- // returns the column index for a given x-coordinate
+ // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate).
+ // This function is used to know which document a column SHOULD be in while it is being dragged.
getColumnFromXCoord = (xCoord: number): number => {
const numColumns = this.columnHeaders.length;
- const coords = this.columnStartXCoords.slice();
+ const coords = [];
+ let colStartXCoord = 0;
+ for (let i = 0; i < numColumns; i++) {
+ coords.push(colStartXCoord);
+ colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.dividerWidth;
+ }
coords.push(this.PanelWidth);
let colIndex = 0;
for (let i = 0; i < numColumns; i++) {
@@ -423,7 +421,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
return colIndex;
};
- // returns the docs of a column based on the x-coordinate provided.
+ // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided.
getDocsFromXCoord = (xCoord: number): Doc[] => {
const colIndex = this.getColumnFromXCoord(xCoord);
const colHeader = StrCast(this.columnHeaders[colIndex].heading);
@@ -455,6 +453,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
};
+ // onInternalDrop is used when dragging and dropping a document within the view, such as dragging
+ // a document to a new column or changing its order within the column.
@undoBatch
@action
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
@@ -464,14 +464,11 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
const rowCol = this.docsDraggedRowCol;
const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note).
const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments;
-
- // const docs = this.childDocs
const docs = this.childDocList;
if (docs && newDocs.length) {
// remove the dragged documents from the childDocList
newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1));
// if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly
- //TODO: you need to update childDocList instead. It seems that childDocs is a copy of the actual array we want to modify
if (rowCol[0] <= 0) {
docs.splice(0, 0, ...newDocs);
} else {
@@ -482,8 +479,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
}
}
- } // it seems like we're creating a link here. Weird. I didn't know that you could establish links by dragging
- else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
+ } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
this.props.addDocument?.(source);
de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
@@ -502,7 +498,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
return true;
}
- // when dropping outside of the current noteTaking context (like a new tab, freeform view, etc...)
+ // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView
+ // to another tab/view/collection
onExternalDrop = async (e: React.DragEvent): Promise<void> => {
const targInd = this.docsDraggedRowCol?.[0] || 0;
const colInd = this.docsDraggedRowCol?.[1] || 0;
@@ -530,12 +527,14 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
headings = () => Array.from(this.Sections);
refList: any[] = [];
+
editableViewProps = () => ({
GetValue: () => '',
SetValue: this.addGroup,
contents: '+ New Column',
});
+ // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column)
sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const type = 'number';
return (
@@ -558,8 +557,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
}}
addDocument={this.addDocument}
- // docsByColumnHeader={this._docsByColumnHeader}
- // setDocsForColHeader={this.setDocsForColHeader}
chromeHidden={this.chromeHidden}
columnHeaders={this.columnHeaders}
Document={this.props.Document}
@@ -569,8 +566,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
numGroupColumns={this.numGroupColumns}
gridGap={this.gridGap}
pivotField={this.notetakingCategoryField}
- columnStartXCoords={this.columnStartXCoords}
- maxColWidth={this.maxColWdith}
+ dividerWidth={this.dividerWidth}
+ maxColWidth={this.maxColWidth}
+ availableWidth={this.availableWidth}
PanelWidth={this.PanelWidth}
key={heading?.heading ?? ''}
headings={this.headings}
@@ -586,19 +584,20 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
);
};
- // called when adding a new columnHeader
+ // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of
+ // columnHeaders and resizing the existing columns to make room for our new one.
@undoBatch
@action
addGroup = (value: string) => {
+ for (const header of this.columnHeaders) {
+ if (header.heading == value) {
+ alert('You cannot use an existing column name. Please try a new column name');
+ return value;
+ }
+ }
const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
- return value && columnHeaders?.push(new SchemaHeaderField(value)) ? true : false;
- };
-
- sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => {
- const descending = StrCast(this.layoutDoc._columnsSort) === 'descending';
- const firstEntry = descending ? b : a;
- const secondEntry = descending ? a : b;
- return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1;
+ const newColWidth = 1 / (this.numGroupColumns + 1);
+ return value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1) ? true : false;
};
onContextMenu = (e: React.MouseEvent): void => {
@@ -612,29 +611,29 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
};
- // used to reset column sizes when using the drag handlers
+ // setColumnStartXCoords is used to update column widths when using the drag handlers between columns
@action
- setColumnStartXCoords = (movementX: number, colIndex: number) => {
- const coords = [...this.columnStartXCoords];
- coords[colIndex] += movementX;
- this.columnStartXCoords = coords;
+ setColumnStartXCoords = (movementXScreen: number, colIndex: number) => {
+ const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0];
+ const leftHeader = this.columnHeaders[colIndex];
+ const rightHeader = this.columnHeaders[colIndex + 1];
+ leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
+ rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth);
};
+ // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view
+ // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that
+ // allows the user to adjust the column widths.
@computed get renderedSections() {
TraceMobx();
- // let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
- // if (this.pivotField) {
- // const entries = Array.from(this.Sections.entries());
- // sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries;
- // }
const entries = Array.from(this.Sections.entries());
- const sections = entries; //.sort(this.sortFunc);
+ const sections = entries;
const eles: JSX.Element[] = [];
for (let i = 0; i < sections.length; i++) {
const col = this.sectionNoteTaking(sections[i][0], sections[i][1]);
eles.push(col);
if (i < sections.length - 1) {
- eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i + 1} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
+ eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
}
}
return eles;
@@ -642,7 +641,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
@computed get buttonMenu() {
const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
- // TODO:glr Allow support for multiple buttons
if (menuDoc) {
const width: number = NumCast(menuDoc._width, 30);
const height: number = NumCast(menuDoc._height, 30);
@@ -693,7 +691,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
@computed get backgroundEvents() {
return SnappingManager.GetIsDragging();
}
+
observer: any;
+
render() {
TraceMobx();
const buttonMenu = this.rootDoc.buttonMenu;
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 624beca96..4610da4e3 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -5,11 +5,12 @@ import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
+import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { ScriptField } from '../../../fields/ScriptField';
+import { Cast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils';
+import { returnEmptyString } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
@@ -21,13 +22,10 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from '../EditableView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionNoteTakingView.scss';
-import { listSpec } from '../../../fields/Schema';
-import { Cast } from '../../../fields/Types';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-// So this is how we are storing a column
interface CSVFieldColumnProps {
Document: Doc;
DataDoc: Opt<Doc>;
@@ -38,7 +36,6 @@ interface CSVFieldColumnProps {
columnHeaders: SchemaHeaderField[] | undefined;
headingObject: SchemaHeaderField | undefined;
yMargin: number;
- // columnWidth: number;
numGroupColumns: number;
gridGap: number;
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
@@ -49,32 +46,32 @@ interface CSVFieldColumnProps {
screenToLocalTransform: () => Transform;
observeHeight: (myref: any) => void;
unobserveHeight: (myref: any) => void;
- //setDraggedCol:(clonedDiv:any, header:SchemaHeaderField, xycoors: )
editableViewProps: () => any;
- resizeColumns: (n: number) => void;
- columnStartXCoords: number[];
+ resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean;
PanelWidth: number;
maxColWidth: number;
- // docsByColumnHeader: Map<string, Doc[]>
- // setDocsForColHeader: (key: string, docs: Doc[]) => void
+ dividerWidth: number;
+ availableWidth: number;
}
+/**
+ * CollectionNoteTakingViewColumn represents an individual column rendered in CollectionNoteTakingView. The
+ * majority of functions here are for rendering styles.
+ */
@observer
export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> {
@observable private _background = 'inherit';
+ // columnWidth returns the width of a column in absolute pixels
@computed get columnWidth() {
- // base cases
- if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length == 1) {
+ if (!this.props.columnHeaders || !this.props.headingObject) {
return this.props.maxColWidth;
}
- const i = this.props.columnHeaders.indexOf(this.props.headingObject);
- if (i < 0 || i > this.props.columnStartXCoords.length - 1) {
+ if (this.props.columnHeaders.length == 1) {
return this.props.maxColWidth;
}
- const endColValue = i == this.props.numGroupColumns - 1 ? this.props.PanelWidth : this.props.columnStartXCoords[i + 1];
- // TODO make the math work here. 35 is half of 70, which is the current width of the divider
- return endColValue - this.props.columnStartXCoords[i] - 30;
+ const i = this.props.columnHeaders.indexOf(this.props.headingObject);
+ return this.props.columnHeaders[i].width * this.props.availableWidth;
}
private dropDisposer?: DragManager.DragDropDisposer;
@@ -84,8 +81,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
@observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb';
_ele: HTMLElement | null = null;
- // This is likely similar to what we will be doing. Why do we need to make these refs?
- // is that the only way to have drop targets?
createColumnDropRef = (ele: HTMLDivElement | null) => {
this.dropDisposer?.();
if (ele) {
@@ -134,6 +129,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
@action pointerLeave = () => (this._background = 'inherit');
textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+ // addNewTextDoc is called when a user starts typing in a column to create a new node
@action
addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
if (!value && !forceEmptyNote) return false;
@@ -146,14 +142,22 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
return this.props.addDocument?.(newDoc) || false;
};
+ // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area.
+ // If the user deletes the first column, the documents get moved to the second column. Otherwise,
+ // all docs are added to the column directly to the left.
@undoBatch
@action
deleteColumn = () => {
const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
if (columnHeaders && this.props.headingObject) {
const index = columnHeaders.indexOf(this.props.headingObject);
- this.props.docList.forEach(d => (d[this.props.pivotField] = 'unset'));
+ const newColIndex = index > 0 ? index - 1 : 1;
+ const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined;
+ const newHeading = newColHeader ? newColHeader.heading : 'unset';
+ this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading));
+ const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0;
columnHeaders.splice(index, 1);
+ this.props.resizeColumns(false, colWidth, index);
}
};
@@ -255,7 +259,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
</div>
</div>
) : null;
- // const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `;
const templatecols = `${this.columnWidth}px `;
const type = this.props.Document.type;
return (
@@ -280,10 +283,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
</div>
{!this.props.chromeHidden && type !== DocumentType.PRES ? (
- <div
- className="collectionNoteTakingView-DocumentButtons"
- // style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}>
- style={{ width: this.columnWidth - 20, marginBottom: 10 }}>
+ <div className="collectionNoteTakingView-DocumentButtons" style={{ width: this.columnWidth - 20, marginBottom: 10 }}>
<div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
<EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
</div>
@@ -311,7 +311,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
key={heading}
style={{
- //TODO: change this so that it's based on the column width
width: this.columnWidth,
background: this._background,
}}
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
index 7d31b3193..8d659f790 100644
--- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -1,5 +1,7 @@
import { action, observable } from 'mobx';
import * as React from 'react';
+import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { UndoManager } from '../../util/UndoManager';
interface DividerProps {
index: number;
@@ -7,34 +9,35 @@ interface DividerProps {
setColumnStartXCoords: (movementX: number, colIndex: number) => void;
}
+/**
+ * CollectionNoteTakingViewDivider are dividers between CollectionNoteTakingViewColumns,
+ * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers
+ * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns.
+ */
export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> {
@observable private isHoverActive = false;
@observable private isResizingActive = false;
@action
private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
- window.removeEventListener('pointermove', this.onPointerMove);
- window.removeEventListener('pointerup', this.onPointerUp);
- window.addEventListener('pointermove', this.onPointerMove);
- window.addEventListener('pointerup', this.onPointerUp);
+ const batch = UndoManager.StartBatch('resizing');
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) => {
+ this.props.setColumnStartXCoords(delta[0], this.props.index);
+ return false;
+ },
+ action(() => {
+ this.isResizingActive = false;
+ this.isHoverActive = false;
+ batch.end();
+ }),
+ emptyFunction
+ );
this.isResizingActive = true;
};
- @action
- private onPointerUp = () => {
- this.isResizingActive = false;
- this.isHoverActive = false;
- window.removeEventListener('pointermove', this.onPointerMove);
- window.removeEventListener('pointerup', this.onPointerUp);
- };
-
- @action
- onPointerMove = ({ movementX }: PointerEvent) => {
- this.props.setColumnStartXCoords(movementX, this.props.index);
- };
-
render() {
return (
<div
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index d4efef47a..71834607c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -455,8 +455,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1));
docs.splice(insertInd - offset, 0, ...newDocs);
}
- // reset drag manager docs, because we just dropped
- DragManager.docsBeingDragged.length = 0;
}
} else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
@@ -493,13 +491,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
targInd = i;
}
});
- super.onExternalDrop(e, {}, () => {
- if (targInd !== -1) {
- const newDoc = this.childDocs[this.childDocs.length - 1];
- const docs = this.childDocList;
- if (docs) {
- docs.splice(docs.length - 1, 1);
- docs.splice(targInd, 0, newDoc);
+ super.onExternalDrop(e, {}, (docs: Doc[]) => {
+ if (targInd === -1) {
+ this.addDocument(docs);
+ }
+ else {
+ const childDocs = this.childDocList;
+ if (childDocs) {
+ childDocs.splice(targInd, 0, ...docs);
}
}
});
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e147f34d2..73574bdb3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -242,43 +242,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const size: number = PresBox.Instance?._selectedArray.size;
const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
- // If pinWithView option set then update scale and x / y props of slide
- if (pinProps?.pinWithView) {
- const viewProps = pinProps.pinWithView;
- pinDoc.presPinView = true;
- pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2;
- pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2;
- pinDoc.presPinViewScale = viewProps.scale;
- pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left + viewProps.bounds.width, viewProps.bounds.top + viewProps.bounds.height]);
- }
- if (pinProps?.pinDocView) {
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking;
- const pannable: boolean = (pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG;
- if (scrollable) {
- const scroll = doc._scrollTop;
- pinDoc.presPinView = true;
- pinDoc.presPinViewScroll = scroll;
- } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) {
- pinDoc.presPinView = true;
- pinDoc.presStartTime = doc._currentTimecode;
- pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1;
- } else if (pannable) {
- pinDoc.presPinView = true;
- pinDoc.presPinViewX = pinDoc._panX;
- pinDoc.presPinViewY = pinDoc._panY;
- pinDoc.presPinViewScale = pinDoc._viewScale;
- const pw = NumCast(pinProps.panelWidth);
- const ph = NumCast(pinProps.panelHeight);
- const ps = NumCast(pinDoc._viewScale);
- if (pw && ph && ps) {
- pinDoc.contentBounds = new List<number>([NumCast(pinDoc.panX) - pw / 2 / ps, NumCast(pinDoc.panY) - ph / 2 / ps, NumCast(pinDoc.panX) + pw / 2 / ps, NumCast(pinDoc.panY) + ph / 2 / ps]);
- }
- } else if (doc.type === DocumentType.COMPARISON) {
- const width = doc._clipWidth;
- pinDoc.presPinClipWidth = width;
- pinDoc.presPinView = true;
- }
- }
+
+ PresBox.pinDocView(pinDoc, pinProps);
pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
if (!pinProps?.audioRange && duration !== undefined) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index ee01c341b..89cc22d07 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -34,6 +34,7 @@ export interface PoolData {
zIndex?: number;
width?: number;
height?: number;
+ backgroundColor?: string;
color?: string;
opacity?: number;
transition?: string;
@@ -45,7 +46,7 @@ export interface PoolData {
export interface ViewDefResult {
ele: JSX.Element;
bounds?: ViewDefBounds;
- inkMask?: boolean;
+ inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden
}
function toLabel(target: FieldResult<Field>) {
if (typeof target === 'number' || Number(target)) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 010132aa5..d80fcdfc3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -20,15 +20,20 @@
pointer-events: none;
}
+.collectionfreeformview-mask-empty,
.collectionfreeformview-mask {
- mix-blend-mode: multiply;
z-index: 5000;
width: $INK_MASK_SIZE;
height: $INK_MASK_SIZE;
transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF);
- background-color: rgba(0, 0, 0, 0.7);
pointer-events: none;
position: absolute;
+ background-color: transparent;
+ transition: background-color 1s ease 0s;
+}
+.collectionfreeformview-mask {
+ mix-blend-mode: multiply;
+ background-color: rgba(0, 0, 0, 0.7);
}
.collectionfreeformview-viewdef {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0fd326091..052cbd3bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -115,13 +115,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _marqueeViewRef = React.createRef<MarqueeView>();
- @observable _keyframeEditing = false;
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
@computed get views() {
- const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask).map(ele => ele.ele);
- const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && !ele.inkMask).map(ele => ele.ele);
- if (viewsMask.length) renderableEles.push(<div className="collectionfreeformview-mask">{viewsMask}</div>);
+ const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele);
+ const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele);
+ if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>);
return renderableEles;
}
@computed get fitToContentVals() {
@@ -185,8 +184,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
}
};
- @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
- getKeyFrameEditing = () => this._keyframeEditing;
onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -251,7 +248,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
isCurrent(doc: Doc) {
- if (doc.isInkMask && doc.opacity === 0) return false; // bcz: hacky --- allows inkMasks to be "turned off" in a presentation without removing them from the collection. otherwise, they still render a gray background.. need to come back to this and fix.
const dispTime = NumCast(doc._timecodeToShow, -1);
const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5);
const curTime = NumCast(this.Document._currentTimecode, -1);
@@ -273,15 +269,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1));
const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000));
- const dropPos = this.Document._currentFrame !== undefined ? [dvals.x || 0, dvals.y || 0] : [NumCast(refDoc.x), NumCast(refDoc.y)];
+ const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)];
for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
const d = docDragData.droppedDocuments[i];
const layoutDoc = Doc.Layout(d);
if (this.Document._currentFrame !== undefined) {
CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false);
const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
- vals.x = x + (vals.x || 0) - dropPos[0];
- vals.y = y + (vals.y || 0) - dropPos[1];
+ vals.x = x + NumCast(vals.x) - dropPos[0];
+ vals.y = y + NumCast(vals.y) - dropPos[1];
vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined;
CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals);
} else {
@@ -1318,22 +1314,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
- const layoutDoc = Doc.Layout(params.pair.layout);
- const { z, color, zIndex } = params.pair.layout;
- const { x, y, opacity } =
- this.Document._currentFrame === undefined
- ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.childOpacity ? this.props.childOpacity() : this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) }
- : CollectionFreeFormDocumentView.getValues(params.pair.layout, NumCast(this.Document._currentFrame));
+ const curFrame = Cast(this.Document._currentFrame, 'number');
+ const childDoc = params.pair.layout;
+ const childDocLayout = Doc.Layout(childDoc);
+ const { z, zIndex } = childDoc;
+ const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
+ const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame);
return {
x: NumCast(x),
y: NumCast(y),
z: Cast(z, 'number'),
- color: StrCast(color),
+ color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
+ backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
+ opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
zIndex: Cast(zIndex, 'number'),
- transition: StrCast(layoutDoc.dataTransition),
- opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number', null),
- width: Cast(layoutDoc._width, 'number'),
- height: Cast(layoutDoc._height, 'number'),
+ width: Cast(childDocLayout._width, 'number'),
+ height: Cast(childDocLayout._height, 'number'),
+ transition: StrCast(childDocLayout.dataTransition),
pair: params.pair,
replica: '',
};
@@ -1448,7 +1445,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
for (const entry of array) {
const lastPos = this._cachedPool.get(entry[0]); // last computed pos
const newPos = entry[1];
- if (!lastPos || newPos.opacity !== lastPos.opacity || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex) {
+ if (
+ !lastPos ||
+ newPos.color !== lastPos.color ||
+ newPos.backgroundColor !== lastPos.backgroundColor ||
+ newPos.opacity !== lastPos.opacity ||
+ newPos.x !== lastPos.x ||
+ newPos.y !== lastPos.y ||
+ newPos.z !== lastPos.z ||
+ newPos.zIndex !== lastPos.zIndex
+ ) {
this._layoutPoolData.set(entry[0], newPos);
}
if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) {
@@ -1465,7 +1471,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
elements.push({
ele: this.getChildDocView(entry[1]),
bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica),
- inkMask: BoolCast(entry[1].pair.layout.isInkMask),
+ inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1,
})
);
@@ -2207,3 +2213,6 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
!readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
});
+ScriptingGlobals.add(function pinWithView(readOnly: boolean) {
+ !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocView: true, panelWidth: view.props.PanelWidth(), panelHeight: view.props.PanelHeight() }));
+});
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index e19e2d525..86566ac6a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -16,9 +16,14 @@ import { StyleProp } from '../StyleProvider';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps } from './DocumentView';
import React = require('react');
+import { InkField } from '../../../fields/InkField';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Field } from '../../util/ProsemirrorCopy/prompt';
+import { RefField } from '../../../fields/RefField';
+import { ObjectField } from '../../../fields/ObjectField';
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+ dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
renderCutoffProvider: (doc: Doc) => boolean;
zIndex?: number;
@@ -32,32 +37,39 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames
+ public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
+ public static animDataFields = ['data', 'text']; // fields that are configured to be animatable using animation frames
@observable _animPos: number[] | undefined = undefined;
@observable _contentView: DocumentView | undefined | null;
get displayName() {
+ // this makes mobx trace() statements more descriptive
return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')';
- } // this makes mobx trace() statements more descriptive
+ }
+
get transform() {
return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`;
}
get X() {
- return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x);
+ return this.dataProvider?.x ?? NumCast(this.Document.x);
}
get Y() {
- return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y);
+ return this.dataProvider?.y ?? NumCast(this.Document.y);
}
get ZInd() {
- return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex);
+ return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
}
get Opacity() {
- return this.dataProvider ? this.dataProvider.opacity : undefined;
+ return this.dataProvider?.opacity;
+ }
+ get BackgroundColor() {
+ return this.dataProvider?.backgroundColor;
+ }
+ get Color() {
+ return this.dataProvider?.color;
}
get Highlight() {
return this.dataProvider?.highlight;
}
- @computed get ShowTitle() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>;
- }
@computed get dataProvider() {
return this.props.dataProvider?.(this.props.Document, this.props.replica);
}
@@ -66,7 +78,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
- if (property === StyleProp.Opacity && doc === this.layoutDoc) return this.Opacity; // only change the opacity for this specific document, not its children
+ if (doc === this.layoutDoc)
+ // prettier-ignore
+ switch (property) {
+ case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children
+ case StyleProp.BackgroundColor: return this.BackgroundColor;
+ case StyleProp.Color: return this.Color;
+ }
return this.props.styleProvider?.(doc, props, property);
};
@@ -77,6 +95,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}, {} as { [val: string]: Opt<number> });
}
+ public static getStringValues(doc: Doc, time: number) {
+ return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => {
+ p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
+ return p;
+ }, {} as { [val: string]: Opt<string> });
+ }
+
public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
@@ -95,6 +120,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
const findexed = Cast(doc[`${val}-indexed`], listSpec('number'), null);
findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
});
+ CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+ const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+ findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+ });
+ CollectionFreeFormDocumentView.animDataFields.forEach(val => {
+ const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+ findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+ });
})
);
setTimeout(
@@ -141,9 +174,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
// opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in
doc['opacity-indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
}
- CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolated(val, 'activeFrame', doc, currTimecode)));
- doc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
- doc.dataTransition = 'inherit';
+ CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedNumber(val, 'activeFrame', doc, currTimecode)));
+ CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
+ CollectionFreeFormDocumentView.animDataFields.forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
+ const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
+ doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
+ targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
+ targetDoc.dataTransition = 'inherit';
});
}
@@ -189,7 +226,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
};
- const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const mixBlendMode = undefined; // (StrCast(this.layoutDoc.mixBlendMode) as any) || (typeof background === 'string' && background && !background.startsWith('linear') && DashColor(background).alpha() !== 1 ? 'multiply' : undefined);
return (
<div
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 381436a56..d065c62fb 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -25,7 +25,7 @@ import './DocumentView.scss';
import { EquationBox } from './EquationBox';
import { FieldView, FieldViewProps } from './FieldView';
import { FilterBox } from './FilterBox';
-import { FormattedTextBox, FormattedTextBoxProps } from './formattedText/FormattedTextBox';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { FunctionPlotBox } from './FunctionPlotBox';
import { ImageBox } from './ImageBox';
import { KeyValueBox } from './KeyValueBox';
@@ -114,14 +114,13 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
@observer
export class DocumentContentsView extends React.Component<
- DocumentViewProps &
- FormattedTextBoxProps & {
- isSelected: (outsideReaction: boolean) => boolean;
- select: (ctrl: boolean) => void;
- NativeDimScaling?: () => number;
- setHeight?: (height: number) => void;
- layoutKey: string;
- }
+ DocumentViewProps & {
+ isSelected: (outsideReaction: boolean) => boolean;
+ select: (ctrl: boolean) => void;
+ NativeDimScaling?: () => number;
+ setHeight?: (height: number) => void;
+ layoutKey: string;
+ }
> {
@computed get layout(): string {
TraceMobx();
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 6ea697a2f..9aaaf1e68 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -99,6 +99,7 @@
border-radius: inherit;
width: 100%;
height: 100%;
+ transition: inherit;
.sharingIndicator {
height: 30px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 172adcafe..74143a731 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1656,7 +1656,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
ref={this.ContentRef}
style={{
transition: this.props.dataTransition,
- //position: this.props.Document.isInkMask ? 'absolute' : undefined,
transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
width: isButton || isPresTreeElement ? '100%' : xshift() ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
height:
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index dd2c13391..77aaa4441 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -26,16 +26,21 @@ export interface FieldViewProps extends DocumentViewSharedProps {
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
onBrowseClick?: () => ScriptField | undefined;
onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
+ pointerEvents?: () => Opt<string>;
// properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
- pointerEvents?: () => Opt<string>;
+ // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields
+ // Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields
+ backgroundColor?: string;
+ color?: string;
fontSize?: number;
height?: number;
width?: number;
- background?: string;
- color?: string;
xPadding?: number;
yPadding?: number;
+ noSidebar?: boolean;
+ dontScale?: boolean;
+ dontSelectOnLoad?: boolean; // suppress selecting (e.g.,. text box) when loaded (and mark as not being associated with scrollTop document field)
}
@observer
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 4b1fbaf7d..d9f46509e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,34 +1,37 @@
-
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, Field, FieldResult } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { RichTextField } from "../../../fields/RichTextField";
-import { listSpec } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, FieldValue, NumCast } from "../../../fields/Types";
-import { ImageField } from "../../../fields/URLField";
-import { Docs } from "../../documents/Documents";
-import { SetupDrag } from "../../util/DragManager";
-import { CompiledScript, CompileScript, ScriptOptions } from "../../util/Scripting";
-import { undoBatch } from "../../util/UndoManager";
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, Field, FieldResult } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { RichTextField } from '../../../fields/RichTextField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { Docs } from '../../documents/Documents';
+import { SetupDrag } from '../../util/DragManager';
+import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
+import { undoBatch } from '../../util/UndoManager';
import { FieldView, FieldViewProps } from './FieldView';
-import "./KeyValueBox.scss";
-import { KeyValuePair } from "./KeyValuePair";
-import React = require("react");
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import e = require("express");
+import './KeyValueBox.scss';
+import { KeyValuePair } from './KeyValuePair';
+import React = require('react');
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import e = require('express');
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { ImageBox } from './ImageBox';
export type KVPScript = {
script: CompiledScript;
- type: "computed" | "script" | false;
+ type: 'computed' | 'script' | false;
onDelegate: boolean;
};
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(KeyValueBox, fieldStr);
+ }
private _mainCont = React.createRef<HTMLDivElement>();
private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
@@ -37,8 +40,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@observable private rows: KeyValuePair[] = [];
- @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
- get fieldDocToLayout() { return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document; }
+ @computed get splitPercentage() {
+ return NumCast(this.props.Document.schemaSplitPercentage, 50);
+ }
+ get fieldDocToLayout() {
+ return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document;
+ }
@action
onEnterKey = (e: React.KeyboardEvent): void => {
@@ -46,19 +53,19 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
e.stopPropagation();
if (this._keyInput.current?.value && this._valInput.current?.value && this.fieldDocToLayout) {
if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput.current.value, this._valInput.current.value)) {
- this._keyInput.current.value = "";
- this._valInput.current.value = "";
+ this._keyInput.current.value = '';
+ this._valInput.current.value = '';
document.body.focus();
}
}
}
- }
+ };
public static CompileKVPScript(value: string): KVPScript | undefined {
- const eq = value.startsWith("=");
+ const eq = value.startsWith('=');
value = eq ? value.substr(1) : value;
- const dubEq = value.startsWith(":=") ? "computed" : value.startsWith(";=") ? "script" : false;
+ const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false;
value = dubEq ? value.substr(2) : value;
- const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: "any", _readOnly_: "boolean" }, editable: false };
+ const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false };
if (dubEq) options.typecheck = false;
const script = CompileScript(value, options);
return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
@@ -67,11 +74,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
const { script, type, onDelegate } = kvpScript;
//const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
- const target = forceOnDelegate || onDelegate || key.startsWith("_") ? doc : doc.proto || doc;
+ const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc;
let field: Field;
- if (type === "computed") {
+ if (type === 'computed') {
field = new ComputedField(script);
- } else if (type === "script") {
+ } else if (type === 'script') {
field = new ScriptField(script);
} else {
const res = script.run({ this: target }, console.log);
@@ -96,7 +103,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
if (e.buttons === 1 && this.props.isSelected(true)) {
e.stopPropagation();
}
- }
+ };
onPointerWheel = (e: React.WheelEvent): void => e.stopPropagation();
rowHeight = () => 30;
@@ -104,7 +111,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@computed get createTable() {
const doc = this.fieldDocToLayout;
if (!doc) {
- return <tr><td>Loading...</td></tr>;
+ return (
+ <tr>
+ <td>Loading...</td>
+ </tr>
+ );
}
const realDoc = doc;
@@ -122,83 +133,102 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let i = 0;
const self = this;
for (const key of Object.keys(ids).slice().sort()) {
- rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight}
- ref={(function () {
- let oldEl: KeyValuePair | undefined;
- return (el: KeyValuePair) => {
- if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
- oldEl = el;
- if (el) self.rows.push(el);
- };
- })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+ rows.push(
+ <KeyValuePair
+ doc={realDoc}
+ addDocTab={this.props.addDocTab}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.rowHeight}
+ ref={(function () {
+ let oldEl: KeyValuePair | undefined;
+ return (el: KeyValuePair) => {
+ if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+ oldEl = el;
+ if (el) self.rows.push(el);
+ };
+ })()}
+ keyWidth={100 - this.splitPercentage}
+ rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')}
+ key={key}
+ keyName={key}
+ />
+ );
}
return rows;
}
@computed get newKeyValue() {
- return <tr className="keyValueBox-valueRow">
- <td className="keyValueBox-td-key" onClick={(e) => { this._keyInput.current!.select(); e.stopPropagation(); }} style={{ width: `${100 - this.splitPercentage}%` }}>
- <input style={{ width: "100%" }} ref={this._keyInput} type="text" placeholder="Key" />
- </td>
- <td className="keyValueBox-td-value" onClick={(e) => { this._valInput.current!.select(); e.stopPropagation(); }} style={{ width: `${this.splitPercentage}%` }}>
- <input style={{ width: "100%" }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} />
- </td>
- </tr>;
+ return (
+ <tr className="keyValueBox-valueRow">
+ <td
+ className="keyValueBox-td-key"
+ onClick={e => {
+ this._keyInput.current!.select();
+ e.stopPropagation();
+ }}
+ style={{ width: `${100 - this.splitPercentage}%` }}>
+ <input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" />
+ </td>
+ <td
+ className="keyValueBox-td-value"
+ onClick={e => {
+ this._valInput.current!.select();
+ e.stopPropagation();
+ }}
+ style={{ width: `${this.splitPercentage}%` }}>
+ <input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} />
+ </td>
+ </tr>
+ );
}
@action
onDividerMove = (e: PointerEvent): void => {
const nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
- }
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
+ };
@action
onDividerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onDividerMove);
+ document.removeEventListener('pointermove', this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- }
+ };
onDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointermove', this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
- }
+ };
- getTemplate = async () => {
- const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template", _chromeHidden: true });
- parent._columnWidth = 100;
- for (const row of this.rows.filter(row => row.isChecked)) {
- await this.createTemplateField(parent, row);
- row.uncheck();
+ getFieldView = async () => {
+ const rows = this.rows.filter(row => row.isChecked);
+ if (rows.length > 1) {
+ const parent = Docs.Create.StackingDocument([], { _autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true });
+ for (const row of rows) {
+ const field = this.createFieldView(DocCast(this.props.Document.data), row);
+ field && Doc.AddDocToList(parent, 'data', field);
+ row.uncheck();
+ }
+ return parent;
}
- return parent;
- }
+ return this.createFieldView(DocCast(this.props.Document.data), rows.lastElement());
+ };
- createTemplateField = async (parentStackingDoc: Doc, row: KeyValuePair) => {
+ createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
const metaKey = row.props.keyName;
- const sourceDoc = await Cast(this.props.Document.data, Doc);
- if (!sourceDoc) {
- return;
- }
+ const fieldTemplate = Doc.MakeAlias(templateDoc);
+ fieldTemplate.title = metaKey;
+ fieldTemplate.layout = this.inferType(templateDoc[metaKey], metaKey);
+ return fieldTemplate;
+ };
- const fieldTemplate = await this.inferType(sourceDoc[metaKey], metaKey);
- if (!fieldTemplate) {
- return;
- }
- const previousViewType = fieldTemplate._viewType;
- Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
- previousViewType && (fieldTemplate._viewType = previousViewType);
-
- Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
- }
-
- inferType = async (data: FieldResult, metaKey: string) => {
+ inferType = (data: FieldResult, metaKey: string) => {
const options = { _width: 300, _height: 300, title: metaKey };
- if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
- return Docs.Create.TextDocument("", options);
+ if (data instanceof RichTextField || typeof data === 'string' || typeof data === 'number') {
+ return FormattedTextBox.LayoutString(metaKey);
} else if (data instanceof List) {
if (data.length === 0) {
return Docs.Create.StackingDocument([], options);
}
- const first = await Cast(data[0], Doc);
+ const first = DocCast(data[0]);
if (!first || !first.data) {
return Docs.Create.StackingDocument([], options);
}
@@ -212,44 +242,52 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
return undefined;
}
} else if (data instanceof ImageField) {
- return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options);
+ return ImageBox.LayoutString(metaKey);
}
- return new Doc;
- }
+ return new Doc();
+ };
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
- const open = cm.findByDescription("Change Perspective...");
- const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
+ const open = cm.findByDescription('Change Perspective...');
+ const openItems: ContextMenuProps[] = open && 'subitems' in open ? open.subitems : [];
openItems.push({
- description: "Default Perspective", event: () => {
- this.props.addDocTab(this.props.Document, "close");
- this.props.addDocTab(this.fieldDocToLayout, "add:right");
- }, icon: "image"
+ description: 'Default Perspective',
+ event: () => {
+ this.props.addDocTab(this.props.Document, 'close');
+ this.props.addDocTab(this.fieldDocToLayout, 'add:right');
+ },
+ icon: 'image',
});
- !open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" });
- }
+ !open && cm.addItem({ description: 'Change Perspective...', subitems: openItems, icon: 'external-link-alt' });
+ };
render() {
- const dividerDragger = this.splitPercentage === 0 ? (null) :
- <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
- <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
- </div>;
-
- return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}>
- <table className="keyValueBox-table">
- <tbody className="keyValueBox-tbody">
- <tr className="keyValueBox-header">
- <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader}
- onPointerDown={SetupDrag(this._keyHeader, this.getTemplate)}
- >Key</th>
- <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>
- </tr>
- {this.createTable}
- {this.newKeyValue}
- </tbody>
- </table>
- {dividerDragger}
- </div>);
+ const dividerDragger =
+ this.splitPercentage === 0 ? null : (
+ <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
+ <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
+ </div>
+ );
+
+ return (
+ <div className="keyValueBox-cont" onWheel={this.onPointerWheel} onContextMenu={this.specificContextMenu} ref={this._mainCont}>
+ <table className="keyValueBox-table">
+ <tbody className="keyValueBox-tbody">
+ <tr className="keyValueBox-header">
+ <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
+ Key
+ </th>
+ <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>
+ Fields
+ </th>
+ </tr>
+ {this.createTable}
+ {this.newKeyValue}
+ </tbody>
+ </table>
+ {dividerDragger}
+ </div>
+ );
}
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index cb68c1ac3..c72b5ca9b 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -90,9 +90,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
@computed get label() {
return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
}
- @computed get icon() {
- return StrCast(this.dataDoc.icon, 'user') as any;
- }
+ Icon = (color: string) => {
+ const icon = StrCast(this.dataDoc.icon, 'user') as any;
+ const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
+ return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
+ };
@computed get dropdown() {
return BoolCast(this.rootDoc.dropDownOpen);
}
@@ -228,7 +230,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
className={`menuButton ${this.type} ${active}`}
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {this.Icon(color)}
{!this.label || !FontIconBox.GetShowLabels() ? null : (
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
{' '}
@@ -252,16 +254,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const script = ScriptCast(this.rootDoc.script);
- if (!script) {
- return null;
- }
let noviceList: string[] = [];
let text: string | undefined;
let dropdown = true;
let icon: IconProp = 'caret-down';
try {
- if (script.script.originalScript.startsWith('setView')) {
+ if (script?.script.originalScript.startsWith('setView')) {
const selected = SelectionManager.Docs().lastElement();
if (selected) {
if (StrCast(selected.type) === DocumentType.COL) {
@@ -277,7 +276,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
text = 'User Default';
}
noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
- } else if (script.script.originalScript.startsWith('setFont')) {
+ } else if (script?.script.originalScript.startsWith('setFont')) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
@@ -287,11 +286,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
// Get items to place into the list
- const list = this.buttonList.map(value => {
- if (Doc.noviceMode && !noviceList.includes(value)) {
- return;
- }
- return (
+ const list = this.buttonList
+ .filter(value => !Doc.noviceMode || noviceList.includes(value))
+ .map(value => (
<div
className="list-item"
key={`${value}`}
@@ -302,8 +299,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
onClick={() => script.script.run({ value }).result}>
{value[0].toUpperCase() + value.slice(1)}
</div>
- );
- });
+ ));
const label =
!this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -384,7 +380,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {this.Icon(color)}
<div className="colorButton-color" style={{ backgroundColor: curColor }} />
{label}
{/* {dropdownCaret} */}
@@ -436,7 +432,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
} else {
return (
<div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {this.Icon(color)}
{label}
</div>
);
@@ -449,11 +445,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
@computed get defaultButton() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const active: string = StrCast(this.rootDoc.dropDownOpen);
return (
<div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
<div className="menuButton-wrap">
- <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} />
+ {this.Icon(color)}
{!this.label || !FontIconBox.GetShowLabels() ? null : (
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
{' '}
@@ -485,90 +480,50 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
render() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const label =
- !this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color, backgroundColor }}>
- {this.label}
- </div>
- );
-
- const menuLabel =
+ const label = (noBackground: boolean = false) =>
!this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color, backgroundColor: 'transparent' }}>
+ <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}>
{this.label}
</div>
);
-
// TODO:glr Add label of button type
- let button: JSX.Element | null = this.defaultButton;
+ let button: JSX.Element = this.defaultButton;
+ // prettier-ignore
switch (this.type) {
- case ButtonType.TextButton:
- button = (
+ case ButtonType.DropdownList: return this.dropdownListButton;
+ case ButtonType.ColorButton: return this.colorButton;
+ case ButtonType.NumberButton: return this.numberButton;
+ case ButtonType.EditableText: return this.editableText;
+ case ButtonType.DropdownButton: button = this.dropdownButton; break;
+ case ButtonType.ToggleButton: button = this.toggleButton; break;
+ case ButtonType.TextButton: button = (
<div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
+ {this.Icon(color)}
{StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
- {label}
+ {label()}
</div>
);
- // button = <TextButton {...buttonProps}></TextButton>
- break;
- case ButtonType.EditableText:
- button = this.editableText;
- break;
- case ButtonType.NumberButton:
- button = this.numberButton;
- break;
- case ButtonType.DropdownButton:
- button = this.dropdownButton;
- break;
- case ButtonType.DropdownList:
- button = this.dropdownListButton;
- break;
- case ButtonType.ColorButton:
- button = this.colorButton;
- break;
- case ButtonType.ToolButton:
- button = (
- <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- {label}
- </div>
- );
- break;
- case ButtonType.ToggleButton:
- button = this.toggleButton;
- // button = <ToggleButton {...buttonProps}></ToggleButton>
break;
case ButtonType.ClickButton:
- button = (
- <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- {label}
+ case ButtonType.ToolButton: button = (
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}>
+ {this.Icon(color)}
+ {label()}
</div>
);
break;
- case ButtonType.MenuButton:
- const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
- button = (
+ case ButtonType.MenuButton: button = (
<div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
- {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />}
- {menuLabel}
+ {this.Icon(color)}
+ {label(true)}
<FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
</div>
);
break;
- default:
- break;
}
- const retval =
- !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? (
- button
- ) : button !== null ? (
- <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>
- ) : null;
- return retval;
+ return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
}
}
@@ -697,7 +652,7 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return RichTextMenu.Instance.fontSize.replace('px', '');
+ return RichTextMenu.Instance?.fontSize.replace('px', '');
}
if (typeof size === 'number') size = size.toString();
if (size && Number(size).toString() === size) size += 'px';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index f61533619..81ac45521 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclReadonly, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -35,6 +35,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
+import { CollectionTreeView } from '../../collections/CollectionTreeView';
import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
@@ -44,6 +45,7 @@ import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
+import { DocumentViewInternal } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { LinkDocPreview } from '../LinkDocPreview';
import { DashDocCommentView } from './DashDocCommentView';
@@ -61,25 +63,14 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
-import { text } from 'body-parser';
-import { CollectionTreeView } from '../../collections/CollectionTreeView';
-import { DocumentViewInternal } from '../DocumentView';
const translateGoogleApi = require('translate-google-api');
-export interface FormattedTextBoxProps {
- makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
- xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
- yPadding?: number;
- noSidebar?: boolean;
- dontScale?: boolean;
- dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field)
-}
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
@@ -1843,18 +1834,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : '';
- const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
- return styleFromString?.height === '0px' ? null : (
+ const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
+ return styleFromLayoutString?.height === '0px' ? null : (
<div
className="formattedTextBox-cont"
onWheel={e => this.props.isContentActive() && e.stopPropagation()}
style={{
- transform: this.props.dontScale ? undefined : `scale(${scale})`,
- transformOrigin: this.props.dontScale ? undefined : 'top left',
- width: this.props.dontScale ? undefined : `${100 / scale}%`,
- height: this.props.dontScale ? undefined : `${100 / scale}%`,
+ ...(this.props.dontScale
+ ? {}
+ : {
+ transform: `scale(${scale})`,
+ transformOrigin: 'top left',
+ width: `${100 / scale}%`,
+ height: `${100 / scale}%`,
+ }),
+ transition: 'inherit',
// overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
- ...styleFromString,
+ color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
+ fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
+ fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily),
+ fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight),
+ ...styleFromLayoutString,
}}>
<div
className={`formattedTextBox-cont`}
@@ -1862,11 +1862,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
style={{
overflow: this.autoHeight ? 'hidden' : undefined,
height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
- background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
- fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
- fontWeight: Cast(this.layoutDoc._fontWeight, 'string', null) as any,
- fontFamily: StrCast(this.layoutDoc._fontFamily, 'inherit'),
pointerEvents: interactive ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 2a77210ae..0cbe60c0c 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -16,7 +16,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
import { FieldViewProps } from '../FieldView';
-import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox';
+import { FormattedTextBox } from './FormattedTextBox';
import { updateBullets } from './ProsemirrorExampleTransfer';
import './RichTextMenu.scss';
import { schema } from './schema_rts';
@@ -29,7 +29,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
private _linkToRef = React.createRef<HTMLInputElement>();
@observable public view?: EditorView;
- public editorProps: (FieldViewProps & FormattedTextBoxProps) | undefined;
+ public editorProps: FieldViewProps | undefined;
public _brushMap: Map<string, Set<Mark>> = new Map();
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 05e09361b..eb40089ec 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -358,20 +358,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false);
};
+ static pinDataTypes(target: Doc) {
+ const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
+ const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
+ const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
+ const clippable = [DocumentType.COMPARISON].includes(target.type as any);
+ return { scrollable, pannable, temporal, clippable };
+ }
// navigates to the bestTarget document by making sure it is on screen,
// then it applies the view specs stored in activeItem to
@action
static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) {
- if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
- bestTarget._scrollTop = activeItem.presPinViewScroll;
- } else if (bestTarget.type === DocumentType.COMPARISON) {
- bestTarget._clipWidth = activeItem.presPinClipWidth;
- } else if ([DocumentType.AUDIO, DocumentType.VID].includes(bestTarget.type as any)) {
- bestTarget._currentTimecode = activeItem.presStartTime;
- } else {
+ bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
+ const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(bestTarget);
+ if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
+ if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
+ if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll;
+ if (pannable) {
const contentBounds = Cast(activeItem.contentBounds, listSpec('number'));
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
if (contentBounds) {
bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2;
bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2;
@@ -388,6 +392,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
}
+ /// copies values from the targetDoc (which is the prototype of the pinDoc) to
+ /// reserved fields on the pinDoc so that those values can be restored to the
+ /// target doc when navigating to it.
+ @action
+ static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined) {
+ if (pinProps?.pinWithView) {
+ // If pinWithView option set then update scale and x / y props of slide
+ const bounds = pinProps.pinWithView.bounds;
+ pinDoc.presPinView = true;
+ pinDoc.presPinViewX = bounds.left + bounds.width / 2;
+ pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+ pinDoc.presPinViewScale = pinProps.pinWithView.scale;
+ pinDoc.contentBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
+ }
+ if (pinProps?.pinDocView) {
+ const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(pinDoc);
+ pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable;
+
+ if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
+ else if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
+ else if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1;
+ else if (pannable) {
+ const panX = NumCast(pinDoc._panX);
+ const panY = NumCast(pinDoc._panY);
+ const pw = NumCast(pinProps.panelWidth);
+ const ph = NumCast(pinProps.panelHeight);
+ const ps = NumCast(pinDoc._viewScale);
+ if (pw && ph && ps) {
+ pinDoc.contentBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]);
+ }
+ pinDoc.presPinViewX = panX;
+ pinDoc.presPinViewY = panY;
+ pinDoc.presPinViewScale = ps;
+ }
+ }
+ }
+
/**
* This method makes sure that cursor navigates to the element that
* has the option open and last in the group.
@@ -1540,26 +1581,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
- @computed get effectDirection(): string {
- let effect = '';
+ @computed get effectDirection() {
+ // prettier-ignore
switch (this.activeItem.presEffectDirection) {
- case 'left':
- effect = 'Enter from left';
- break;
- case 'right':
- effect = 'Enter from right';
- break;
- case 'top':
- effect = 'Enter from top';
- break;
- case 'bottom':
- effect = 'Enter from bottom';
- break;
- default:
- effect = 'Enter from center';
- break;
+ case 'left': return 'Enter from left';
+ case 'right': return 'Enter from right';
+ case 'top': return'Enter from top';
+ case 'bottom': return 'Enter from bottom';
}
- return effect;
+ return 'Enter from center';
}
@undoBatch
@@ -1582,179 +1612,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
};
- @computed get presPinViewOptionsDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 16, filter: 'invert(1)' }} />;
- return (
- <>
- {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : null}
- <div className="ribbon-doubleButton">
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{activeItem.presPinView ? 'Turn off pin with view' : 'Turn on pin with view'}</div>
- </>
- }>
- <div
- className="ribbon-toggle"
- style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : '' }}
- onClick={() => {
- activeItem.presPinView = !activeItem.presPinView;
- targetDoc.presPinView = activeItem.presPinView;
- if (activeItem.presPinView) {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinView = true;
- activeItem.presPinViewScroll = scroll;
- } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
- activeItem.presStartTime = targetDoc._currentTimecode;
- activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1;
- } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinView = true;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const width = targetDoc._clipWidth;
- activeItem.presPinClipWidth = width;
- activeItem.presPinView = true;
- }
- }
- }}>
- {presPinWithViewIcon}
- </div>
- </Tooltip>
- {activeItem.presPinView ? (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Update the pinned view with the view of the selected document'}</div>
- </>
- }>
- <div
- className="ribbon-button"
- onClick={() => {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
- activeItem.presStartTime = targetDoc._currentTimecode;
- activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- } else {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
- }
- }}>
- Update
- </div>
- </Tooltip>
- ) : null}
- </div>
- </>
- );
- }
-
- @computed get panOptionsDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- return (
- <>
- {this.panable ? (
- <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Pan X</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number"
- value={NumCast(activeItem.presPinViewX)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- const val = e.target.value;
- activeItem.presPinViewX = Number(val);
- })}
- />
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Pan Y</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number"
- value={NumCast(activeItem.presPinViewY)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- const val = e.target.value;
- activeItem.presPinViewY = Number(val);
- })}
- />
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Scale</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number"
- value={NumCast(activeItem.presPinViewScale)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- const val = e.target.value;
- activeItem.presPinViewScale = Number(val);
- })}
- />
- </div>
- </div>
- </div>
- ) : null}
- </>
- );
- }
-
- @computed get scrollOptionsDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- return (
- <>
- {this.scrollable ? (
- <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Scroll</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number"
- value={NumCast(activeItem.presPinViewScroll)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- const val = e.target.value;
- activeItem.presPinViewScroll = Number(val);
- })}
- />
- </div>
- </div>
- </div>
- ) : null}
- </>
- );
- }
-
@computed get mediaStopSlides() {
const activeItem: Doc = this.activeItem;
const list = this.childDocs.map((doc, i) => {
@@ -2102,42 +1959,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
createTemplate = (layout: string, input?: string) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- let x = 0;
- let y = 0;
- if (activeItem && targetDoc) {
- x = NumCast(targetDoc.x);
- y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20;
- }
- let doc = undefined;
- const title = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
- const subtitle = Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
- const header = Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
- const contentTitle = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
- const content = Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
- const content1 = Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
- const content2 = Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
+ const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
+ const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
+ const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
+ const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
+ const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
+ const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
+ const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
+ const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
+ const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
+ // prettier-ignore
switch (layout) {
- case 'blank':
- doc = Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x: x, y: y });
- break;
- case 'title':
- doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
- break;
- case 'header':
- doc = Docs.Create.FreeformDocument([header], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
- break;
- case 'content':
- doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
- break;
- case 'twoColumns':
- doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
- break;
- default:
- break;
+ case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y });
+ case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+ case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+ case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
+ case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y })
}
- return doc;
};
// Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view)
@@ -2201,42 +2039,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/**
* Returns the collection type as a string for headers
*/
- @computed get stringType(): string {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- let type: string = '';
- if (activeItem) {
- switch (targetDoc.type) {
- case DocumentType.PDF:
- type = 'PDF';
- break;
- case DocumentType.RTF:
- type = 'Text node';
- break;
- case DocumentType.COL:
- type = 'Collection';
- break;
- case DocumentType.AUDIO:
- type = 'Audio';
- break;
- case DocumentType.VID:
- type = 'Video';
- break;
- case DocumentType.IMG:
- type = 'Image';
- break;
- case DocumentType.WEB:
- type = 'Web page';
- break;
- case DocumentType.MAP:
- type = 'Map';
- break;
- default:
- type = 'Other node';
- break;
+ @computed get stringType() {
+ if (this.activeItem) {
+ // prettier-ignore
+ switch (this.targetDoc.type) {
+ case DocumentType.PDF: return 'PDF';
+ case DocumentType.RTF: return 'Text node';
+ case DocumentType.COL: return 'Collection';
+ case DocumentType.AUDIO: return 'Audio';
+ case DocumentType.VID: return 'Video';
+ case DocumentType.IMG: return 'Image';
+ case DocumentType.WEB: return 'Web page';
+ case DocumentType.MAP: return 'Map';
+ default: return 'Other node';
}
}
- return type;
+ return '';
}
@observable private openActiveColorPicker: boolean = false;
@@ -2845,10 +2663,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
this.updateList(activeItem.frameList);
break;
- case DocumentType.COL:
- break;
- default:
- break;
}
};
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 3af8cad9a..91196ca21 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -256,10 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerMove = (e: PointerEvent) => {
const slide = this._itemRef.current!;
- let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
- for (const doc of DragManager.docsBeingDragged) {
- if (!doc.presentationTargetDoc) dragIsPresItem = false;
- }
+ const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc);
if (slide && dragIsPresItem) {
const rect = slide.getBoundingClientRect();
const y = e.clientY - rect.top; //y position within the element.
@@ -501,7 +498,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
}}>
- <div className="presItem-name" style={{ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
+ <div
+ className="presItem-name"
+ style={{
+ pointerEvents: isSelected ? undefined : 'none',
+ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105,
+ cursor: isSelected ? 'text' : 'grab',
+ }}>
<div>{`${this.indexInPres + 1}. `}</div>
<EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
</div>
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 3fc0a237e..cbcfed06f 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -1,5 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
+import { MdBugReport } from 'react-icons/md';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -18,6 +19,7 @@ import { DashboardView } from '../DashboardView';
import { Borders, Colors } from '../global/globalEnums';
import { MainView } from '../MainView';
import './TopBar.scss';
+import { ReportManager } from '../../util/ReportManager';
/**
* ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
@@ -31,6 +33,7 @@ export class TopBar extends React.Component {
DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
});
};
+
render() {
const activeDashboard = Doc.ActiveDashboard;
return (
@@ -92,6 +95,9 @@ export class TopBar extends React.Component {
{GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'view original'}
</div>
) : null}
+ <div className="topbar-button-icon" onClick={() => ReportManager.Instance.open()}>
+ <MdBugReport />
+ </div>
<div className="topbar-button-icon" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}>
<FontAwesomeIcon icon="question-circle" />
</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 8d56ebf8c..612fc7fb8 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1389,6 +1389,20 @@ export namespace Doc {
return !curPres ? false : DocListCast(curPres.data).findIndex(val => Doc.AreProtosEqual(val, doc)) !== -1;
}
+ export function styleFromLayoutString(rootDoc: Doc, layoutDoc: Doc, props: any, scale: number) {
+ const style: { [key: string]: any } = {};
+ const divKeys = ['width', 'height', 'fontSize', 'transform', 'left', 'backgroundColor', 'left', 'right', 'top', 'bottom', 'pointerEvents', 'position'];
+ const replacer = (match: any, expr: string, offset: any, string: any) => {
+ // bcz: this executes a script to convert a property expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: rootDoc, this: layoutDoc, scale }).result?.toString() ?? '';
+ };
+ divKeys.map((prop: string) => {
+ const p = props[prop];
+ typeof p === 'string' && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+ });
+ return style;
+ }
+
// prettier-ignore
export function toIcon(doc?: Doc, isOpen?: boolean) {
switch (StrCast(doc?.type)) {
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 3b02d0cfe..1321bc327 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,60 +1,60 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, primitive } from 'serializr';
+import { ObjectField } from './ObjectField';
+import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
export const PastelSchemaPalette = new Map<string, string>([
// ["pink1", "#FFB4E8"],
- ["pink2", "#ff9cee"],
- ["pink3", "#ffccf9"],
- ["pink4", "#fcc2ff"],
- ["pink5", "#f6a6ff"],
- ["purple1", "#b28dff"],
- ["purple2", "#c5a3ff"],
- ["purple3", "#d5aaff"],
- ["purple4", "#ecd4ff"],
+ ['pink2', '#ff9cee'],
+ ['pink3', '#ffccf9'],
+ ['pink4', '#fcc2ff'],
+ ['pink5', '#f6a6ff'],
+ ['purple1', '#b28dff'],
+ ['purple2', '#c5a3ff'],
+ ['purple3', '#d5aaff'],
+ ['purple4', '#ecd4ff'],
// ["purple5", "#fb34ff"],
- ["purple6", "#dcd3ff"],
- ["purple7", "#a79aff"],
- ["purple8", "#b5b9ff"],
- ["purple9", "#97a2ff"],
- ["bluegreen1", "#afcbff"],
- ["bluegreen2", "#aff8db"],
- ["bluegreen3", "#c4faf8"],
- ["bluegreen4", "#85e3ff"],
- ["bluegreen5", "#ace7ff"],
+ ['purple6', '#dcd3ff'],
+ ['purple7', '#a79aff'],
+ ['purple8', '#b5b9ff'],
+ ['purple9', '#97a2ff'],
+ ['bluegreen1', '#afcbff'],
+ ['bluegreen2', '#aff8db'],
+ ['bluegreen3', '#c4faf8'],
+ ['bluegreen4', '#85e3ff'],
+ ['bluegreen5', '#ace7ff'],
// ["bluegreen6", "#6eb5ff"],
- ["bluegreen7", "#bffcc6"],
- ["bluegreen8", "#dbffd6"],
- ["yellow1", "#f3ffe3"],
- ["yellow2", "#e7ffac"],
- ["yellow3", "#ffffd1"],
- ["yellow4", "#fff5ba"],
+ ['bluegreen7', '#bffcc6'],
+ ['bluegreen8', '#dbffd6'],
+ ['yellow1', '#f3ffe3'],
+ ['yellow2', '#e7ffac'],
+ ['yellow3', '#ffffd1'],
+ ['yellow4', '#fff5ba'],
// ["red1", "#ffc9de"],
- ["red2", "#ffabab"],
- ["red3", "#ffbebc"],
- ["red4", "#ffcbc1"],
- ["orange1", "#ffd5b3"],
- ["gray", "#f1efeb"]
+ ['red2', '#ffabab'],
+ ['red3', '#ffbebc'],
+ ['red4', '#ffcbc1'],
+ ['orange1', '#ffd5b3'],
+ ['gray', '#f1efeb'],
]);
export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)];
export const DarkPastelSchemaPalette = new Map<string, string>([
- ["pink2", "#c932b0"],
- ["purple4", "#913ad6"],
- ["bluegreen1", "#3978ed"],
- ["bluegreen7", "#2adb3e"],
- ["bluegreen5", "#21b0eb"],
- ["yellow4", "#edcc0c"],
- ["red2", "#eb3636"],
- ["orange1", "#f2740f"],
+ ['pink2', '#c932b0'],
+ ['purple4', '#913ad6'],
+ ['bluegreen1', '#3978ed'],
+ ['bluegreen7', '#2adb3e'],
+ ['bluegreen5', '#21b0eb'],
+ ['yellow4', '#edcc0c'],
+ ['red2', '#eb3636'],
+ ['orange1', '#f2740f'],
]);
@scriptingGlobal
-@Deserializable("schemaheader")
+@Deserializable('schemaheader')
export class SchemaHeaderField extends ObjectField {
@serializable(primitive())
heading: string;
@@ -69,7 +69,7 @@ export class SchemaHeaderField extends ObjectField {
@serializable(primitive())
desc: boolean | undefined; // boolean determines sort order, undefined when no sort
- constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
+ constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
super();
this.heading = heading;
@@ -111,7 +111,7 @@ export class SchemaHeaderField extends ObjectField {
}
[Copy]() {
- return new SchemaHeaderField(this.heading, this.color, this.type);
+ return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed);
}
[ToScriptString]() {
@@ -120,4 +120,4 @@ export class SchemaHeaderField extends ObjectField {
[ToString]() {
return `SchemaHeaderField`;
}
-} \ No newline at end of file
+}
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 68fb45987..48d5c5563 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc';
import { Copy, Id, ToScriptString, ToString } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
-import { Cast, NumCast } from './Types';
+import { Cast, NumCast, StrCast } from './Types';
import { Plugins } from './util';
function optional(propSchema: PropSchema) {
@@ -189,7 +189,7 @@ export class ComputedField extends ScriptField {
const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ComputedField(compiled) : undefined;
}
- public static MakeInterpolated(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+ public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
if (!doc[`${fieldKey}-indexed`]) {
const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]);
flist[curTimecode] = NumCast(doc[fieldKey]);
@@ -199,6 +199,26 @@ export class ComputedField extends ScriptField {
const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
}
+ public static MakeInterpolatedString(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+ if (!doc[`${fieldKey}-indexed`]) {
+ const flist = new List<string>(numberRange(curTimecode + 1).map(i => undefined) as any as string[]);
+ flist[curTimecode] = StrCast(doc[fieldKey]);
+ doc[`${fieldKey}-indexed`] = flist;
+ }
+ const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
+ const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
+ return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+ }
+ public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+ if (!doc[`${fieldKey}-indexed`]) {
+ const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]);
+ flist[curTimecode] = Field.Copy(doc[fieldKey]);
+ doc[`${fieldKey}-indexed`] = flist;
+ }
+ const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
+ const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
+ return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+ }
}
export namespace ComputedField {
let useComputed = true;
diff --git a/src/fields/util.ts b/src/fields/util.ts
index d87bb6656..b3cbbe241 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -445,8 +445,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
undo: action(() => {
// console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo
diff.items.forEach((item: any) => {
- const ind = receiver[prop].indexOf(item.value ? item.value() : item);
- ind !== -1 && receiver[prop].splice(ind, 1);
+ if (item instanceof SchemaHeaderField) {
+ const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
+ ind !== -1 && receiver[prop].splice(ind, 1);
+ } else {
+ const ind = receiver[prop].indexOf(item.value ? item.value() : item);
+ ind !== -1 && receiver[prop].splice(ind, 1);
+ }
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
}),