aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSophie Zhang <sophie_zhang@brown.edu>2023-04-13 01:21:11 -0400
committerSophie Zhang <sophie_zhang@brown.edu>2023-04-13 01:21:11 -0400
commit65781ccf9a3f19578cf4a51bca65b35c05c0795b (patch)
treeedd0988aa6e545878b3b6c3ffddf00c62e346ef0 /src
parentdb582e135fceb6162d0c9cf00e2580fb1349fddb (diff)
parent5d1e3710a015d8915bd367ece753817d84d9d916 (diff)
Merge branch 'master' into pres-trail-sophie
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/JSZipUtils.js142
-rw-r--r--src/Utils.ts17
-rw-r--r--src/client/documents/Documents.ts93
-rw-r--r--src/client/documents/Gitlike.ts226
-rw-r--r--src/client/util/CurrentUserUtils.ts58
-rw-r--r--src/client/util/DictationManager.ts10
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/DragManager.ts56
-rw-r--r--src/client/util/DropConverter.ts10
-rw-r--r--src/client/util/LinkFollower.ts32
-rw-r--r--src/client/util/Scripting.ts8
-rw-r--r--src/client/util/SelectionManager.ts45
-rw-r--r--src/client/util/SettingsManager.tsx4
-rw-r--r--src/client/views/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/views/ContextMenu.tsx12
-rw-r--r--src/client/views/DocComponent.tsx18
-rw-r--r--src/client/views/DocumentButtonBar.tsx43
-rw-r--r--src/client/views/DocumentDecorations.tsx55
-rw-r--r--src/client/views/EditableView.tsx4
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/InkStrokeProperties.ts1
-rw-r--r--src/client/views/InkingStroke.tsx18
-rw-r--r--src/client/views/MainView.tsx40
-rw-r--r--src/client/views/MarqueeAnnotator.tsx9
-rw-r--r--src/client/views/OverlayView.tsx6
-rw-r--r--src/client/views/PreviewCursor.tsx10
-rw-r--r--src/client/views/PropertiesButtons.tsx10
-rw-r--r--src/client/views/PropertiesView.tsx83
-rw-r--r--src/client/views/SidebarAnnos.tsx21
-rw-r--r--src/client/views/StyleProvider.scss15
-rw-r--r--src/client/views/StyleProvider.tsx131
-rw-r--r--src/client/views/TemplateMenu.tsx3
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx8
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx154
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx8
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx25
-rw-r--r--src/client/views/collections/CollectionMenu.tsx19
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx7
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx7
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss2
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx103
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx49
-rw-r--r--src/client/views/collections/CollectionSubView.tsx14
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx2
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx24
-rw-r--r--src/client/views/collections/CollectionView.tsx42
-rw-r--r--src/client/views/collections/TabDocView.tsx18
-rw-r--r--src/client/views/collections/TreeView.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx363
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx7
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx9
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss10
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx20
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx3
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx3
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx683
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx513
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx138
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx152
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss707
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx1245
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx62
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx136
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx694
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx69
-rw-r--r--src/client/views/global/globalCssVariables.scss3
-rw-r--r--src/client/views/global/globalCssVariables.scss.d.ts2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx2
-rw-r--r--src/client/views/nodes/AudioBox.tsx17
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx1
-rw-r--r--src/client/views/nodes/ColorBox.tsx2
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx6
-rw-r--r--src/client/views/nodes/DataViz.tsx20
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx181
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss41
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx319
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.scss (renamed from src/client/views/nodes/DataVizBox/TableBox.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx105
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts67
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx15
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx8
-rw-r--r--src/client/views/nodes/DocumentView.scss6
-rw-r--r--src/client/views/nodes/DocumentView.tsx1009
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx21
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx42
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx11
-rw-r--r--src/client/views/nodes/LabelBox.tsx3
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx34
-rw-r--r--src/client/views/nodes/LinkBox.tsx55
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx8
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx5
-rw-r--r--src/client/views/nodes/PDFBox.tsx160
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx177
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx18
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx20
-rw-r--r--src/client/views/nodes/VideoBox.tsx42
-rw-r--r--src/client/views/nodes/WebBox.tsx17
-rw-r--r--src/client/views/nodes/button/ButtonScripts.ts16
-rw-r--r--src/client/views/nodes/button/FontIconBox.scss30
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx82
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx57
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx16
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx16
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx12
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx73
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts28
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts8
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx16
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx4
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx2
-rw-r--r--src/client/views/pdf/Annotation.tsx6
-rw-r--r--src/client/views/pdf/PDFViewer.tsx20
-rw-r--r--src/client/views/search/SearchBox.tsx2
-rw-r--r--src/fields/Doc.ts241
-rw-r--r--src/fields/RichTextField.ts26
-rw-r--r--src/fields/RichTextUtils.ts10
-rw-r--r--src/fields/ScriptField.ts2
-rw-r--r--src/fields/documentSchemas.ts1
-rw-r--r--src/server/ApiManagers/DataVizManager.ts26
-rw-r--r--src/server/ApiManagers/UploadManager.ts28
-rw-r--r--src/server/DataVizUtils.ts26
-rw-r--r--src/server/index.ts15
134 files changed, 4137 insertions, 6021 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 717b68f3e..8f0e384aa 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/JSZipUtils.js b/src/JSZipUtils.js
new file mode 100644
index 000000000..5ce1bd471
--- /dev/null
+++ b/src/JSZipUtils.js
@@ -0,0 +1,142 @@
+var JSZipUtils = {};
+// just use the responseText with xhr1, response with xhr2.
+// The transformation doesn't throw away high-order byte (with responseText)
+// because JSZip handles that case. If not used with JSZip, you may need to
+// do it, see https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
+JSZipUtils._getBinaryFromXHR = function (xhr) {
+ // for xhr.responseText, the 0xFF mask is applied by JSZip
+ return xhr.response || xhr.responseText;
+};
+
+// taken from jQuery
+function createStandardXHR() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch (e) {}
+}
+
+function createActiveXHR() {
+ try {
+ return new window.ActiveXObject('Microsoft.XMLHTTP');
+ } catch (e) {}
+}
+
+// Create the request object
+var createXHR =
+ typeof window !== 'undefined' && window.ActiveXObject
+ ? /* Microsoft failed to properly
+ * implement the XMLHttpRequest in IE7 (can't request local files),
+ * so we use the ActiveXObject when it is available
+ * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+ * we need a fallback.
+ */
+ function () {
+ return createStandardXHR() || createActiveXHR();
+ }
+ : // For all other browsers, use the standard XMLHttpRequest object
+ createStandardXHR;
+
+/**
+ * @param {string} path The path to the resource to GET.
+ * @param {function|{callback: function, progress: function}} options
+ * @return {Promise|undefined} If no callback is passed then a promise is returned
+ */
+JSZipUtils.getBinaryContent = function (path, options) {
+ var promise, resolve, reject;
+ var callback;
+
+ if (!options) {
+ options = {};
+ }
+
+ // backward compatible callback
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ } else if (typeof options.callback === 'function') {
+ // callback inside options object
+ callback = options.callback;
+ }
+
+ if (!callback && typeof Promise !== 'undefined') {
+ promise = new Promise(function (_resolve, _reject) {
+ resolve = _resolve;
+ reject = _reject;
+ });
+ } else {
+ resolve = function (data) {
+ callback(null, data);
+ };
+ reject = function (err) {
+ callback(err, null);
+ };
+ }
+
+ /*
+ * Here is the tricky part : getting the data.
+ * In firefox/chrome/opera/... setting the mimeType to 'text/plain; charset=x-user-defined'
+ * is enough, the result is in the standard xhr.responseText.
+ * cf https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Receiving_binary_data_in_older_browsers
+ * In IE <= 9, we must use (the IE only) attribute responseBody
+ * (for binary data, its content is different from responseText).
+ * In IE 10, the 'charset=x-user-defined' trick doesn't work, only the
+ * responseType will work :
+ * http://msdn.microsoft.com/en-us/library/ie/hh673569%28v=vs.85%29.aspx#Binary_Object_upload_and_download
+ *
+ * I'd like to use jQuery to avoid this XHR madness, but it doesn't support
+ * the responseType attribute : http://bugs.jquery.com/ticket/11461
+ */
+ try {
+ var xhr = createXHR();
+
+ xhr.open('GET', path, true);
+
+ // recent browsers
+ if ('responseType' in xhr) {
+ xhr.responseType = 'arraybuffer';
+ }
+
+ // older browser
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType('text/plain; charset=x-user-defined');
+ }
+
+ xhr.onreadystatechange = function (event) {
+ // use `xhr` and not `this`... thanks IE
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200 || xhr.status === 0) {
+ try {
+ resolve(JSZipUtils._getBinaryFromXHR(xhr));
+ } catch (err) {
+ reject(new Error(err));
+ }
+ } else {
+ reject(new Error('Ajax error for ' + path + ' : ' + this.status + ' ' + this.statusText));
+ }
+ }
+ };
+
+ if (options.progress) {
+ xhr.onprogress = function (e) {
+ options.progress({
+ path: path,
+ originalEvent: e,
+ percent: (e.loaded / e.total) * 100,
+ loaded: e.loaded,
+ total: e.total,
+ });
+ };
+ }
+
+ xhr.send();
+ } catch (e) {
+ reject(new Error(e), null);
+ }
+
+ // returns a promise or undefined depending on whether a callback was
+ // provided
+ return promise;
+};
+
+// export
+module.exports = JSZipUtils;
diff --git a/src/Utils.ts b/src/Utils.ts
index ae1478943..0c7deaf5d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -8,8 +8,12 @@ import { Message } from './server/Message';
import Color = require('color');
export namespace Utils {
+ export let CLICK_TIME = 300;
export let DRAG_THRESHOLD = 4;
export let SNAP_THRESHOLD = 10;
+ export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) {
+ return Date.now() - downTime < Utils.CLICK_TIME && Math.abs(x - downX) < Utils.DRAG_THRESHOLD && Math.abs(y - downY) < Utils.DRAG_THRESHOLD;
+ }
export function readUploadedFileAsText(inputFile: File) {
const temporaryFileReader = new FileReader();
@@ -509,11 +513,22 @@ export function returnTrue() {
return true;
}
+export function returnAlways(): 'always' {
+ return 'always';
+}
+export function returnNever(): 'never' {
+ return 'never';
+}
+
+export function returnDefault(): 'default' {
+ return 'default';
+}
+
export function returnFalse() {
return false;
}
-export function returnAll() {
+export function returnAll(): 'all' {
return 'all';
}
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 457811e26..8d97ce5af 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -21,6 +21,7 @@ import { Networking } from '../Network';
import { DocumentManager } from '../util/DocumentManager';
import { DragManager, dropActionType } from '../util/DragManager';
import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
+import { FollowLinkScript } from '../util/LinkFollower';
import { LinkManager } from '../util/LinkManager';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { undoBatch, UndoManager } from '../util/UndoManager';
@@ -166,6 +167,7 @@ export class DocumentOptions {
_chromeHidden?: boolean; // whether the editing chrome for a document is hidden
_searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view)
_forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active)
+ enableDragWhenActive?: boolean; // allow dragging even if document contentts are active (e.g., tree, groups)
_stayInCollection?: boolean; // whether the document should remain in its collection when someone tries to drag and drop it elsewhere
_raiseWhenDragged?: boolean; // whether a document is brought to front when dragged.
_hideContextMenu?: boolean; // whether the context menu can be shown
@@ -221,6 +223,8 @@ export class DocumentOptions {
recording?: boolean; // whether WebCam is recording or not
autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline.
dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it.
+ linkSource?: Doc; // the source document for a collection of backlinks
+ updateContentsScript?: ScriptField; // reactive script invoked when viewing a document that can update contents of a collection (or do anything)
toolTip?: string; // tooltip to display on hover
toolType?: string; // type of pen tool
expertMode?: boolean; // something available only in expert (not novice) mode
@@ -228,7 +232,8 @@ export class DocumentOptions {
contextMenuScripts?: List<ScriptField>;
contextMenuLabels?: List<string>;
contextMenuIcons?: List<string>;
- allowClickBeforeDoubleClick?: boolean; // whether a click function can fire before the timeout for a double click has expired
+ defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
+ waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
description?: string; // added for links
layout?: string | Doc; // default layout string for a document
@@ -245,6 +250,7 @@ export class DocumentOptions {
childContextMenuIcons?: List<string>;
followLinkZoom?: boolean; // whether to zoom to the target of a link
hideLinkButton?: boolean; // whether the blue link counter button should be hidden
+ disableDocBrushing?: boolean; // whether to suppress border highlighting
hideDecorationTitle?: boolean;
hideOpenButton?: boolean;
hideResizeHandles?: boolean;
@@ -258,8 +264,8 @@ export class DocumentOptions {
caption?: RichTextField;
opacity?: number;
defaultBackgroundColor?: string;
- _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked
_linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint
+ hideLinkAnchors?: boolean; // suppresses link anchor dots from being displayed
isFolder?: boolean;
lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
activeFrame?: number; // the active frame of a document in a frame base collection
@@ -309,6 +315,7 @@ export class DocumentOptions {
linearViewExpandable?: boolean; // can linear view be expanded
linearViewToggleButton?: string; // button to open close linear view group
linearViewSubMenu?: boolean;
+ linearBtnWidth?: number;
flexGap?: number; // Linear view flex gap
flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse';
@@ -407,6 +414,7 @@ export namespace Docs {
nativeDimModifiable: true,
nativeHeightUnfrozen: true,
forceReflow: true,
+ defaultDoubleClick: 'ignore',
},
},
],
@@ -435,7 +443,7 @@ export namespace Docs {
DocumentType.WEB,
{
layout: { view: WebBox, dataField: defaultDataKey },
- options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
+ options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' },
},
],
[
@@ -463,7 +471,7 @@ export namespace Docs {
DocumentType.AUDIO,
{
layout: { view: AudioBox, dataField: defaultDataKey },
- options: { _height: 100, forceReflow: true, nativeDimModifiable: true },
+ options: { _height: 100, fitWidth: true, forceReflow: true, nativeDimModifiable: true },
},
],
[
@@ -500,12 +508,13 @@ export namespace Docs {
layout: { view: LinkBox, dataField: defaultDataKey },
options: {
childDontRegisterViews: true,
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
+ hideLinkAnchors: true,
_height: 150,
description: '',
showCaption: 'description',
backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
- _removeDropProperties: new List(['isLinkButton']),
+ _removeDropProperties: new List(['onClick']),
},
},
],
@@ -577,14 +586,14 @@ export namespace Docs {
DocumentType.PRES,
{
layout: { view: PresBox, dataField: defaultDataKey },
- options: {},
+ options: { defaultDoubleClick: 'ignore', hideLinkAnchors: true },
},
],
[
DocumentType.FONTICON,
{
- layout: { view: FontIconBox, dataField: defaultDataKey },
- options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%' },
+ layout: { view: FontIconBox, dataField: 'icon' },
+ options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, hideLinkButton: true, _width: 40, _height: 40 },
},
],
[
@@ -761,7 +770,6 @@ export namespace Docs {
...(template.options || {}),
layout: layout.view?.LayoutString(layout.dataField),
data: template.data,
- layout_keyValue: KeyValueBox.LayoutString(''),
};
Object.entries(options).map(pair => {
if (typeof pair[1] === 'string' && pair[1].startsWith('@')) {
@@ -927,13 +935,13 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
- export function LinkDocument(source: { doc: Doc; ctx?: Doc }, target: { doc: Doc; ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
+ export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
const linkDoc = InstanceFromProto(
Prototypes.get(DocumentType.LINK),
undefined,
{
- anchor1: source.doc,
- anchor2: target.doc,
+ anchor1: source,
+ anchor2: target,
...options,
},
id
@@ -983,6 +991,7 @@ export namespace Docs {
I.author = Doc.CurrentUserEmail;
I.rotation = 0;
I.data = new InkField(points);
+ I.defaultDoubleClick = 'click';
I.creationDate = new DateField();
I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
//I['acl-Override'] = SharingPermissions.Unset;
@@ -1020,9 +1029,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id);
}
- export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
- }
+ // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
+ // export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+ // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
+ // }
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id);
@@ -1285,16 +1295,14 @@ export namespace DocUtils {
broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
return DocUtils.ActiveRecordings.map(audio => {
const sourceDoc = getSourceDoc();
- const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor(true) || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
- link && (link.followLinkLocation = OpenWhere.addRight);
- return link;
+ return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { linkDisplay: false, linkRelationship: 'recording annotation:linked recording', description: 'recording timeline' });
});
}
- export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = '', description: string = '', id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
- if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
- const sv = DocumentManager.Instance.getDocumentView(source.doc);
- if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return;
+ export function MakeLink(source: Doc, target: Doc, linkSettings: { linkRelationship?: string; description?: string; linkDisplay?: boolean }, id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
+ if (!linkSettings.linkRelationship) linkSettings.linkRelationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
+ const sv = DocumentManager.Instance.getDocumentView(source);
+ if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target) return;
if (target.doc === Doc.UserDoc()) return undefined;
const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
@@ -1334,16 +1342,16 @@ export namespace DocUtils {
target,
{
title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
- 'anchor1-useLinkSmallAnchor': source.doc.useLinkSmallAnchor ? true : undefined,
- 'anchor2-useLinkSmallAnchor': target.doc.useLinkSmallAnchor ? true : undefined,
+ 'anchor1-useLinkSmallAnchor': source.useLinkSmallAnchor ? true : undefined,
+ 'anchor2-useLinkSmallAnchor': target.useLinkSmallAnchor ? true : undefined,
'acl-Public': SharingPermissions.Augment,
'_acl-Public': SharingPermissions.Augment,
- linkDisplay: true,
+ linkDisplay: linkSettings.linkDisplay,
_linkAutoMove: true,
- linkRelationship,
+ linkRelationship: linkSettings.linkRelationship,
_showCaption: 'description',
_showTitle: 'linkRelationship',
- description,
+ description: linkSettings.description,
},
id
),
@@ -1411,41 +1419,39 @@ export namespace DocUtils {
export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
let created: Doc | undefined;
- let layout: ((fieldKey: string) => string) | undefined;
const field = target[fieldKey];
- const resolved = options || {};
+ const resolved = options ?? {};
if (field instanceof ImageField) {
created = Docs.Create.ImageDocument(field.url.href, resolved);
- layout = ImageBox.LayoutString;
+ created.layout = ImageBox.LayoutString(fieldKey);
} else if (field instanceof Doc) {
created = field;
} else if (field instanceof VideoField) {
created = Docs.Create.VideoDocument(field.url.href, resolved);
- layout = VideoBox.LayoutString;
+ created.layout = VideoBox.LayoutString(fieldKey);
} else if (field instanceof PdfField) {
created = Docs.Create.PdfDocument(field.url.href, resolved);
- layout = PDFBox.LayoutString;
+ created.layout = PDFBox.LayoutString(fieldKey);
} else if (field instanceof AudioField) {
created = Docs.Create.AudioDocument(field.url.href, resolved);
- layout = AudioBox.LayoutString;
+ created.layout = AudioBox.LayoutString(fieldKey);
} else if (field instanceof RecordingField) {
created = Docs.Create.RecordingDocument(field.url.href, resolved);
- layout = RecordingBox.LayoutString;
+ created.layout = RecordingBox.LayoutString(fieldKey);
} else if (field instanceof InkField) {
created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved);
- layout = InkingStroke.LayoutString;
+ created.layout = InkingStroke.LayoutString(fieldKey);
} else if (field instanceof List && field[0] instanceof Doc) {
created = Docs.Create.StackingDocument(DocListCast(field), resolved);
- layout = CollectionView.LayoutString;
+ created.layout = CollectionView.LayoutString(fieldKey);
} else if (field instanceof MapField) {
created = Docs.Create.MapDocument(DocListCast(field), resolved);
- layout = MapBox.LayoutString;
+ created.layout = MapBox.LayoutString(fieldKey);
} else {
created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
- layout = FormattedTextBox.LayoutString;
+ created.layout = FormattedTextBox.LayoutString(fieldKey);
}
if (created) {
- created.layout = layout?.(fieldKey);
created.title = fieldKey;
proto && created.proto && (created.proto = Doc.GetProto(proto));
}
@@ -1541,7 +1547,7 @@ export namespace DocUtils {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
.filter(btnDoc => !btnDoc.hidden)
.map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
- .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail)
+ .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz)
.map((dragDoc, i) => ({
description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
event: undoBatch((args: { x: number; y: number }) => {
@@ -1685,14 +1691,15 @@ export namespace DocUtils {
x: Cast(doc.x, 'number', null),
y: Cast(doc.y, 'number', null),
backgroundColor: '#ACCEF7',
+ hideAllLinks: true,
_width: 15,
_height: 15,
_xPadding: 0,
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
_timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
});
Doc.AddDocToList(context, annotationField, pushpin);
- const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, 'pushpin', '');
+ const pushpinLink = DocUtils.MakeLink(pushpin, doc, { linkRelationship: 'pushpin' }, '');
doc._timecodeToShow = undefined;
return pushpin;
}
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index 575c984f5..5e2baf924 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,118 +1,118 @@
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { Cast, DateCast } from "../../fields/Types";
-import { DateField } from "../../fields/DateField";
-import { Id } from "../../fields/FieldSymbols";
+// import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
+// import { List } from "../../fields/List";
+// import { Cast, DateCast } from "../../fields/Types";
+// import { DateField } from "../../fields/DateField";
+// import { Id } from "../../fields/FieldSymbols";
-// synchs matching documents on the two branches that are being merged/pulled
-// currently this just synchs the main 'fieldKey' component of the data since
-// we don't have individual timestamps for all fields -- this is a problematic design issue.
-function GitlikeSynchDocs(bd: Doc, md: Doc) {
- const fieldKey = Doc.LayoutFieldKey(md);
- const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
- const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
- const bdproto = bd && Doc.GetProto(bd);
- if (bdate !== mdate && bdate <= mdate) {
- if (bdproto && md) {
- bdproto[fieldKey] = Field.Copy(md[fieldKey]);
- bdproto[`${fieldKey}-lastModified`] = new DateField();
- }
- }
- const bldate = DateCast(bd._lastModified)?.date;
- const mldate = DateCast(md._lastModified)?.date;
- if (bldate === mldate || bldate > mldate) return;
- if (bdproto && md) {
- bd.x = Field.Copy(md.x);
- bd.y = Field.Copy(md.y);
- bd.width = Field.Copy(md.width);
- bd.height = Field.Copy(md.height);
- bdproto._lastModified = new DateField();
- }
-}
+// // synchs matching documents on the two branches that are being merged/pulled
+// // currently this just synchs the main 'fieldKey' component of the data since
+// // we don't have individual timestamps for all fields -- this is a problematic design issue.
+// function GitlikeSynchDocs(bd: Doc, md: Doc) {
+// const fieldKey = Doc.LayoutFieldKey(md);
+// const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
+// const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
+// const bdproto = bd && Doc.GetProto(bd);
+// if (bdate !== mdate && bdate <= mdate) {
+// if (bdproto && md) {
+// bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+// bdproto[`${fieldKey}-lastModified`] = new DateField();
+// }
+// }
+// const bldate = DateCast(bd._lastModified)?.date;
+// const mldate = DateCast(md._lastModified)?.date;
+// if (bldate === mldate || bldate > mldate) return;
+// if (bdproto && md) {
+// bd.x = Field.Copy(md.x);
+// bd.y = Field.Copy(md.y);
+// bd.width = Field.Copy(md.width);
+// bd.height = Field.Copy(md.height);
+// bdproto._lastModified = new DateField();
+// }
+// }
-// pulls documents onto a branch from the branch's master
-// if a document exists on master but not on the branch, it is branched and added
-// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
-async function GitlikePullFromMaster(branch: Doc, suffix = "") {
- const masterMain = Cast(branch.branchOf, Doc, null);
- // get the set of documents on both the branch and master
- const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
- const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
- // get the master documents that correspond to the branch documents
- const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
- const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
- // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
- const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
- const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
- oldDocsFromMaster?.forEach(md => {
- const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
- bd && GitlikeSynchDocs(bd, md);
- });
- const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
- // make branch clones of them, then add them to the branch
- const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
- newlyBranchedDocs.forEach(nd => {
- Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
- nd.context = branch;
- });
- // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.
- const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
- // so then remove all the deleted main docs from this branch.
- remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
-}
+// // pulls documents onto a branch from the branch's master
+// // if a document exists on master but not on the branch, it is branched and added
+// // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
+// async function GitlikePullFromMaster(branch: Doc, suffix = "") {
+// const masterMain = Cast(branch.branchOf, Doc, null);
+// // get the set of documents on both the branch and master
+// const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
+// const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+// // get the master documents that correspond to the branch documents
+// const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
+// const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
+// // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
+// const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+// const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+// oldDocsFromMaster?.forEach(md => {
+// const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
+// bd && GitlikeSynchDocs(bd, md);
+// });
+// const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
+// // make branch clones of them, then add them to the branch
+// const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
+// newlyBranchedDocs.forEach(nd => {
+// Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
+// nd.context = branch;
+// });
+// // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.
+// const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
+// // so then remove all the deleted main docs from this branch.
+// remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
+// }
-// merges all branches from the master branch by first merging the top-level collection of documents,
-// and then merging all the annotations on those documents.
-// TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
-async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
- const branches = await DocListCastAsync(master.branches);
- branches?.map(async branch => {
- const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
- branchChildren && await Promise.all(branchChildren.map(async bd => {
- const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
- // see if the branch's child exists on master.
- const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
- // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
- // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
- // on the branch child and master clone.
- if (masterChild.branchOf) {
- const branchDocProto = Doc.GetProto(bd);
- const masterChildProto = Doc.GetProto(masterChild);
- const branchTitle = bd.title;
- branchDocProto.title = masterChildProto.title;
- masterChildProto.title = branchTitle;
- masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
- masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
- Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
- branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
- }
- Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
- masterChild.context = master;
- GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
- }));
- const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
- masterChildren?.forEach(mc => { // see if any master children
- if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch.
- Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
- mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
- }
- });
- });
-}
+// // merges all branches from the master branch by first merging the top-level collection of documents,
+// // and then merging all the annotations on those documents.
+// // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
+// async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
+// const branches = await DocListCastAsync(master.branches);
+// branches?.map(async branch => {
+// const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+// branchChildren && await Promise.all(branchChildren.map(async bd => {
+// const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
+// // see if the branch's child exists on master.
+// const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
+// // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
+// // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
+// // on the branch child and master clone.
+// if (masterChild.branchOf) {
+// const branchDocProto = Doc.GetProto(bd);
+// const masterChildProto = Doc.GetProto(masterChild);
+// const branchTitle = bd.title;
+// branchDocProto.title = masterChildProto.title;
+// masterChildProto.title = branchTitle;
+// masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+// masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
+// Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
+// branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
+// }
+// Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
+// masterChild.context = master;
+// GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
+// }));
+// const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
+// masterChildren?.forEach(mc => { // see if any master children
+// if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch.
+// Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
+// mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
+// }
+// });
+// });
+// }
-// performs a "git"-like task: pull or merge
-// if pull, then target is a specific branch document that will be updated from its associated master
-// if merge, then target is the master doc that will merge in all branches associated with it.
-// TODO: parameterize 'merge' to specify which branch(es) should be merged.
-// extend 'merge' to allow a specific branch to be merge target (not just master);
-// make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
-export async function BranchTask(target: Doc, action: "pull" | "merge") {
- const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
- await func(target, "");
- await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
- await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
-}
+// // performs a "git"-like task: pull or merge
+// // if pull, then target is a specific branch document that will be updated from its associated master
+// // if merge, then target is the master doc that will merge in all branches associated with it.
+// // TODO: parameterize 'merge' to specify which branch(es) should be merged.
+// // extend 'merge' to allow a specific branch to be merge target (not just master);
+// // make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
+// export async function BranchTask(target: Doc, action: "pull" | "merge") {
+// const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
+// await func(target, "");
+// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
+// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
+// }
-export async function BranchCreate(target: Doc) {
- return (await Doc.MakeClone(target, false, true)).clone;
-} \ No newline at end of file
+// export async function BranchCreate(target: Doc) {
+// return (await Doc.MakeClone(target, false, true)).clone;
+// }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2820c66ee..76ea3e3ea 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -25,6 +25,7 @@ import { OpenWhere } from "../views/nodes/DocumentView";
import { OverlayView } from "../views/OverlayView";
import { DragManager } from "./DragManager";
import { MakeTemplate } from "./DropConverter";
+import { FollowLinkScript } from "./LinkFollower";
import { LinkManager } from "./LinkManager";
import { ScriptingGlobals } from "./ScriptingGlobals";
import { ColorScheme } from "./SettingsManager";
@@ -41,11 +42,13 @@ interface Button {
numBtnMax?: number;
switchToggle?: boolean;
width?: number;
+ linearBtnWidth?: number;
toolType?: string; // type of pen tool
expertMode?: boolean;// available only in expert mode
btnList?: List<string>;
ignoreClick?: boolean;
buttonText?: string;
+ backgroundColor?: string;
// fields that do not correspond to DocumentOption fields
scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
@@ -61,7 +64,7 @@ export class CurrentUserUtils {
const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
{
btnOpts: { title: "slide", icon: "address-card" },
- templateOpts: { _width: 400, _height: 300, title: "slideView", childDocumentsActive: true, _xMargin: 3, _yMargin: 3, system: true },
+ templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true },
template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
@@ -94,7 +97,7 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = {
title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true,
- _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, childDocumentsActive: true,
+ _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
@@ -148,7 +151,7 @@ export class CurrentUserUtils {
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true};
+ const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
@@ -183,10 +186,10 @@ export class CurrentUserUtils {
case DocumentType.IMG : creator = imageBox; break;
case DocumentType.FONTICON: creator = fontBox; break;
}
- const allopts = {system: true, ...opts};
+ const allopts = {system: true, onClickScriptDisable: "never", ...opts};
return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ?
DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
- {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)"});
+ {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", });
};
const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
@@ -224,11 +227,11 @@ export class CurrentUserUtils {
{
type: "paragraph", attrs: {}, content: [{
type: "dashField",
- attrs: { fieldKey: "author", docid: "", hideKey: false },
+ attrs: { fieldKey: "author", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}, {
type: "dashField",
- attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+ attrs: { fieldKey: "creationDate", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}]
}]
@@ -269,9 +272,9 @@ export class CurrentUserUtils {
{key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _fitWidth: true, _showSidebar: true, }},
{key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }},
{key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
- {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
+ {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, onClick: FollowLinkScript()}},
{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: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, treeViewHideUnrendered: true}},
{key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _fitWidth: true, _backgroundGridShow: true, }},
@@ -300,7 +303,7 @@ export class CurrentUserUtils {
{ toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
{ toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay},
{ toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true },
- { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", clickFactory: "repl" as any, openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", clickFactory: 'repl' as any, openFactoryLocation: OpenWhere.overlay},
].map(tuple => (
{ openFactoryLocation: OpenWhere.addRight,
scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)',
@@ -313,7 +316,7 @@ export class CurrentUserUtils {
const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
- _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true,
+ _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true,
btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
_removeDropProperties: new List<string>(["_stayInCollection"]),
};
@@ -323,7 +326,7 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = {
title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- childDocumentsActive: true, childDropAction: 'alias'
+ childDropAction: 'alias'
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -608,6 +611,22 @@ export class CurrentUserUtils {
return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
}
+ static freeTools(): Button[] {
+ return [
+ { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+ { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+ ]
+ }
+ static viewTools(): Button[] {
+ return [
+ { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Snap\xA0Lines",icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snap lines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "View\xA0All", icon: "object-group",toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Arrange", icon: "window", toolTip: "Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Reset", icon: "check", toolTip: "Reset View", btnType: ButtonType.ClickButton, expertMode: false, backgroundColor:"transparent", scripts: { onClick: 'resetView()'}}, // Only when floating document is selected in freeform
+ ]
+ }
static textTools():Button[] {
return [
{ title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
@@ -666,17 +685,20 @@ 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, expertMode: false, toolType:"tab", funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
- { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, toolType:"tab", 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, expertMode: false, toolType:"tab", ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
+ { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 20, scripts: { onClick: 'pinWithView(altKey)'}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, 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, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
{ title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
+ { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag",btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(_readOnly_)'}}, // Only when floating document is selected in freeform
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
{ title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
{ title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
{ title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
- { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
+ { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
];
}
@@ -684,10 +706,10 @@ export class CurrentUserUtils {
static setupContextMenuButton(params:Button, btnDoc?:Doc) {
const reqdOpts:DocumentOptions = {
...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- backgroundColor: params.scripts?.onClick ? undefined: "transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background
+ backgroundColor: params.backgroundColor ??"transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background
color: Colors.WHITE, system: true, dontUndo: true,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
- _height: 30, _nativeHeight: 30,
+ _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
toolType: params.toolType, expertMode: params.expertMode,
_stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
_removeDropProperties: new List<string>([ "_stayInCollection"]),
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 203d4ad62..e116ae14b 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -324,16 +324,6 @@ export namespace DictationManager {
],
[
- 'open fields',
- {
- action: (target: DocumentView) => {
- const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
- target.props.addDocTab(kvp, OpenWhere.addRight);
- },
- },
- ],
-
- [
'new outline',
{
action: (target: DocumentView) => {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index f2c554866..ccf370662 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -262,9 +262,11 @@ export class DocumentManager {
finished?: () => void
) => {
const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
+ if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
let rootContextView = await new Promise<DocumentView>(res => {
const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
+ options.didMove = true;
docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation as OpenWhere);
this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
});
@@ -299,7 +301,7 @@ export class DocumentManager {
PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc);
- if (options.toggleTarget) docView.rootDoc.hidden = !docView.rootDoc.hidden;
+ if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden;
if (options.effect) docView.rootDoc[AnimationSym] = options.effect;
if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a56f87075..7d2aa813f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
import { action, observable, runInAction } from 'mobx';
import { DateField } from '../../fields/DateField';
-import { Doc, Field, Opt } from '../../fields/Doc';
+import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { listSpec } from '../../fields/Schema';
@@ -10,7 +10,9 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types
import { emptyFunction, Utils } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import * as globalCssVariables from '../views/global/globalCssVariables.scss';
+import { Colors } from '../views/global/globalEnums';
import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
@@ -149,10 +151,14 @@ export namespace DragManager {
linkDragView: DocumentView;
}
export class ColumnDragData {
- constructor(colKey: SchemaHeaderField) {
- this.colKey = colKey;
+ // constructor(colKey: SchemaHeaderField) {
+ // this.colKey = colKey;
+ // }
+ // colKey: SchemaHeaderField;
+ constructor(colIndex: number) {
+ this.colIndex = colIndex;
}
- colKey: SchemaHeaderField;
+ colIndex: number;
}
// used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made.
// this is pretty clunky and should be rethought out using linkDrag or DocumentDrag
@@ -162,7 +168,6 @@ export namespace DragManager {
this.dropDocCreator = dropDocCreator;
this.offset = [0, 0];
}
- linkSourceDoc?: Doc;
dropDocCreator: (annotationOn: Doc | undefined) => Doc;
dropDocument?: Doc;
offset: number[];
@@ -201,24 +206,26 @@ export namespace DragManager {
dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
- docDragData.droppedDocuments = await Promise.all(
- dragData.draggedDocuments.map(async d =>
- !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
- ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
- : docDragData.dropAction === 'alias'
- ? Doc.BestAlias(d)
- : docDragData.dropAction === 'proto'
- ? Doc.GetProto(d)
- : docDragData.dropAction === 'copy'
- ? (
- await Doc.MakeClone(d)
- ).clone
- : d
+ docDragData.droppedDocuments = (
+ await Promise.all(
+ dragData.draggedDocuments.map(async d =>
+ !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
+ ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
+ : docDragData.dropAction === 'alias'
+ ? Doc.BestAlias(d)
+ : docDragData.dropAction === 'proto'
+ ? Doc.GetProto(d)
+ : docDragData.dropAction === 'copy'
+ ? (
+ await Doc.MakeClone(d)
+ ).clone
+ : d
+ )
)
- );
+ ).filter(d => d);
!['same', 'proto'].includes(docDragData.dropAction as any) &&
docDragData.droppedDocuments.forEach((drop: Doc, i: number) => {
- const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec('string'), []);
+ const dragProps = StrListCast(dragData.draggedDocuments[i].removeDropProperties);
const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps));
remProps.map(prop => (drop[prop] = undefined));
});
@@ -256,8 +263,8 @@ export namespace DragManager {
}
// drags a column from a schema view
- export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], dragData, downX, downY, options);
+ export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(ele, dragData, downX, downY, options);
}
export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -590,3 +597,8 @@ export namespace DragManager {
endDrag?.();
}
}
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
+ if (readOnly) return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE : 'transparent';
+ DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+});
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 7c209d1e0..2e1d6ba23 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -33,15 +33,7 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
let any = false;
docs.forEach(d => {
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;
- } else {
- any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
- }
+ any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
} else if (d.type === DocumentType.COL || d.data instanceof RichTextField) {
any = makeTemplate(d, false) || any;
}
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index eacbcc0e3..d4d7c66f5 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,16 +1,15 @@
-import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
+import { action, observable, runInAction } from 'mobx';
+import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc';
+import { ScriptField } from '../../fields/ScriptField';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
-import { DocumentDecorations } from '../views/DocumentDecorations';
-import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } from '../views/nodes/DocumentView';
+import { DocFocusOptions, OpenWhere } from '../views/nodes/DocumentView';
import { PresBox } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
import { LinkManager } from './LinkManager';
+import { ScriptingGlobals } from './ScriptingGlobals';
import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
/*
* link doc:
* - anchor1: doc
@@ -24,18 +23,19 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
* - user defined kvps
*/
export class LinkFollower {
+ @observable public static IsFollowing = false;
// follows a link - if the target is on screen, it highlights/pans to it.
// if the target isn't onscreen, then it will open up the target in the lightbox, or in place
// depending on the followLinkLocation property of the source (or the link itself as a fallback);
- public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => {
+ public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
const batch = UndoManager.StartBatch('follow link click');
- runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
+ runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
LinkFollower.traverseLink(
linkDoc,
sourceDoc,
action(() => {
batch.end();
- Doc.AddUnHighlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
+ Doc.AddUnHighlightWatcher(action(() => (LinkFollower.IsFollowing = false)));
}),
altKey ? true : undefined
);
@@ -120,3 +120,15 @@ export class LinkFollower {
});
}
}
+
+ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
+ SelectionManager.DeselectAll();
+ LinkFollower.FollowLink(undefined, doc, altKey);
+});
+
+export function FollowLinkScript() {
+ return ScriptField.MakeScript('followLink(this,altKey)', { altKey: 'boolean' });
+}
+export function IsFollowLinkScript(field: FieldResult<Field>) {
+ return ScriptCast(field)?.script.originalScript.includes('followLink(');
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index d32298c83..f17a98616 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,7 +7,9 @@
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import * as ts from 'typescript';
import { Doc, Field } from '../../fields/Doc';
+import { ToScriptString } from '../../fields/FieldSymbols';
import { ObjectField } from '../../fields/ObjectField';
+import { RefField } from '../../fields/RefField';
import { ScriptField } from '../../fields/ScriptField';
import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
@@ -180,7 +182,11 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const captured = options.capturedVariables ?? {};
- const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, '');
+ const signature = Object.keys(captured).reduce((p, v) => {
+ const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`;
+ if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture);
+ return p + formatCapture(captured[v]);
+ }, '');
const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
if (found) return found as CompiledScript;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 0f4f77588..313c255a0 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,3 +1,4 @@
+import { ModalManager } from '@material-ui/core';
import { action, observable, ObservableMap } from 'mobx';
import { computedFn } from 'mobx-utils';
import { Doc, Opt } from '../../fields/Doc';
@@ -10,7 +11,8 @@ import { ScriptingGlobals } from './ScriptingGlobals';
export namespace SelectionManager {
class Manager {
@observable IsDragging: boolean = false;
- SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ SelectedViewsMap: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ @observable SelectedViews: DocumentView[] = [];
@observable SelectedSchemaDocument: Doc | undefined;
@action
@@ -20,7 +22,7 @@ export namespace SelectionManager {
@action
SelectView(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
+ if (!manager.SelectedViewsMap.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
if (!ctrlPressed) {
if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
LinkManager.currentLink = undefined;
@@ -28,33 +30,38 @@ export namespace SelectionManager {
this.DeselectAll();
}
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
docView.props.whenChildContentsActiveChanged(true);
- } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) {
- Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
+ } else if (!ctrlPressed && (Array.from(manager.SelectedViewsMap.entries()).length > 1 || manager.SelectedSchemaDocument)) {
+ Array.from(manager.SelectedViewsMap.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
manager.SelectedSchemaDocument = undefined;
- manager.SelectedViews.clear();
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.length = 0;
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
}
}
@action
- DeselectView(docView: DocumentView): void {
- if (manager.SelectedViews.get(docView)) {
- manager.SelectedViews.delete(docView);
+ DeselectView(docView?: DocumentView): void {
+ if (docView && manager.SelectedViewsMap.get(docView)) {
+ manager.SelectedViewsMap.delete(docView);
+ manager.SelectedViews.splice(manager.SelectedViews.indexOf(docView), 1);
docView.props.whenChildContentsActiveChanged(false);
}
}
@action
DeselectAll(): void {
manager.SelectedSchemaDocument = undefined;
- Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
- manager.SelectedViews.clear();
+ Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.length = 0;
}
}
const manager = new Manager();
- export function DeselectView(docView: DocumentView): void {
+ export function DeselectView(docView?: DocumentView): void {
manager.DeselectView(docView);
}
export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
@@ -67,7 +74,7 @@ export namespace SelectionManager {
const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) {
// wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
- return manager.SelectedViews.get(doc) ? true : false;
+ return manager.SelectedViewsMap.get(doc) ? true : false;
});
// computed functions, such as used in IsSelected generate errors if they're called outside of a
// reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature
@@ -76,7 +83,7 @@ export namespace SelectionManager {
return !doc
? false
: outsideReaction
- ? manager.SelectedViews.get(doc)
+ ? manager.SelectedViewsMap.get(doc)
? true
: false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
: IsSelectedCache(doc);
@@ -85,7 +92,7 @@ export namespace SelectionManager {
export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
- for (const view of Array.from(manager.SelectedViews.keys())) {
+ for (const view of Array.from(manager.SelectedViewsMap.keys())) {
if (view.props.Document === except) found = view;
}
}
@@ -95,13 +102,15 @@ export namespace SelectionManager {
}
export function Views(): Array<DocumentView> {
- return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews;
+ // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
}
export function SelectedSchemaDoc(): Doc | undefined {
return manager.SelectedSchemaDocument;
}
export function Docs(): Doc[] {
- return Array.from(manager.SelectedViews.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+ // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
}
}
ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 6c823e80a..396d754b6 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -184,10 +184,6 @@ export class SettingsManager extends React.Component<{}> {
<div className="preferences-check">Show full toolbar</div>
</div>
<div>
- <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} checked={DragManager.GetRaiseWhenDragged()} />
- <div className="preferences-check">Raise on drag</div>
- </div>
- <div>
<input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} />
<div className="preferences-check">Show button labels</div>
</div>
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index e4ac87aad..5e39387b8 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 6a530e3ae..e4c3e864b 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -102,13 +102,11 @@ export class ContextMenu extends React.Component {
}
}
@action
- moveAfter(item: ContextMenuProps, after: ContextMenuProps) {
- if (after && this.findByDescription(after.description)) {
- const curInd = this._items.findIndex(i => i.description === item.description);
- this._items.splice(curInd, 1);
- const afterInd = this._items.findIndex(i => i.description === after.description);
- this._items.splice(afterInd + 1, 0, item);
- }
+ moveAfter(item: ContextMenuProps, after?: ContextMenuProps) {
+ const curInd = this._items.findIndex(i => i.description === item.description);
+ this._items.splice(curInd, 1);
+ const afterInd = after && this.findByDescription(after.description) ? this._items.findIndex(i => i.description === after.description) : this._items.length;
+ this._items.splice(afterInd, 0, item);
}
@action
setDefaultItem(prefix: string, item: (name: string) => void) {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 7c81d92d4..9fc1487a0 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -16,6 +16,7 @@ import { Touchable } from './Touchable';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
export interface DocComponentProps {
Document: Doc;
+ fieldKey?: string;
LayoutTemplate?: () => Opt<Doc>;
LayoutTemplateString?: string;
}
@@ -37,6 +38,10 @@ export function DocComponent<P extends DocComponentProps>() {
@computed get dataDoc() {
return this.props.Document[DataSym] as Doc;
}
+ // key where data is stored
+ @computed get fieldKey() {
+ return this.props.fieldKey;
+ }
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
@@ -77,12 +82,6 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
return this.props.fieldKey;
}
- isContentActive = (outsideReaction?: boolean) =>
- this.props.isContentActive?.() === false
- ? false
- : Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction)
- ? true
- : undefined;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
@@ -130,13 +129,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
isAnyChildContentActive = () => this._isAnyChildContentActive;
- isContentActive = (outsideReaction?: boolean) =>
- this.props.isContentActive?.() === false
- ? false
- : Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction) || this.isAnyChildContentActive()
- ? true
- : undefined;
-
lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index af868ba9c..8c2ced55a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../fields/Doc';
import { RichTextField } from '../../fields/RichTextField';
@@ -11,6 +11,7 @@ import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import { Docs, DocUtils } from '../documents/Documents';
import { DragManager } from '../util/DragManager';
+import { IsFollowLinkScript } from '../util/LinkFollower';
import { SelectionManager } from '../util/SelectionManager';
import { SharingManager } from '../util/SharingManager';
import { undoBatch, UndoManager } from '../util/UndoManager';
@@ -24,11 +25,9 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
+import { PinProps } from './nodes/trails';
import { TemplateMenu } from './TemplateMenu';
import React = require('react');
-import { DocumentType } from '../documents/DocumentTypes';
-import { FontIconBox } from './nodes/button/FontIconBox';
-import { PinProps } from './nodes/trails';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -238,11 +237,12 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
</Tooltip>
);
};
+ const followLink = IsFollowLinkScript(targetDoc?.onClick);
return !targetDoc ? null : (
<Tooltip title={<div className="dash-tooltip">Set onClick to follow primary link</div>}>
<div
className="documentButtonBar-icon documentButtonBar-follow"
- style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
+ style={{ backgroundColor: followLink ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: followLink ? Colors.BLACK : Colors.WHITE }}
onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false)))}>
<div className="documentButtonBar-followTypes">
{followBtn(
@@ -289,7 +289,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
@observable subEndLink = '';
@computed
get endLinkButton() {
- const linkBtn = (pinDocLayout: boolean, pinDocContent: boolean, icon: IconProp) => {
+ const linkBtn = (pinLayout: boolean, pinContent: boolean, icon: IconProp) => {
const tooltip = `Finish Link and Save ${this.subEndLink} data`;
return !this.view0 ? null : (
<Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
@@ -300,7 +300,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
key={icon.toString()}
size="sm"
icon={icon}
- onPointerEnter={action(e => (this.subEndLink = (pinDocLayout ? 'Layout' : '') + (pinDocLayout && pinDocContent ? ' &' : '') + (pinDocContent ? ' Content' : '')))}
+ onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))}
onPointerLeave={action(e => (this.subEndLink = ''))}
onClick={e => {
const docs = this.props
@@ -309,9 +309,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
.map(dv => dv!.rootDoc);
this.view0 &&
DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.props.Document, true, this.view0, {
- pinDocLayout,
- pinDocContent,
- pinData: !pinDocContent ? {} : { poslayoutview: true, dataannos: true, dataview: true },
+ pinDocLayout: pinLayout,
+ pinData: !pinContent ? {} : { poslayoutview: true, dataannos: true, dataview: pinContent },
} as PinProps);
e.stopPropagation();
@@ -337,7 +336,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
@computed
get pinButton() {
const targetDoc = this.view0?.props.Document;
- const pinBtn = (pinDocLayout: boolean, pinDocContent: boolean, icon: IconProp) => {
+ const pinBtn = (pinLayoutView: boolean, pinContentView: boolean, icon: IconProp) => {
const tooltip = `Pin Document and Save ${this.subPin} to trail`;
return !tooltip ? null : (
<Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
@@ -351,10 +350,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
onPointerEnter={action(
e =>
(this.subPin =
- (pinDocLayout ? 'Layout' : '') +
- (pinDocLayout && pinDocContent ? ' &' : '') +
- (pinDocContent ? ' Content View' : '') +
- (pinDocLayout && pinDocContent ? '(shift+alt)' : pinDocLayout ? '(shift)' : pinDocContent ? '(alt)' : ''))
+ (pinLayoutView ? 'Layout' : '') +
+ (pinLayoutView && pinContentView ? ' &' : '') +
+ (pinContentView ? ' Content View' : '') +
+ (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''))
)}
onPointerLeave={action(e => (this.subPin = ''))}
onClick={e => {
@@ -364,8 +363,8 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
.map(dv => dv!.rootDoc);
TabDocView.PinDoc(docs, {
pinAudioPlay: true,
- pinDocLayout,
- pinData: { dataview: true },
+ pinDocLayout: pinLayoutView,
+ pinData: { dataview: pinContentView },
activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null),
currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null),
});
@@ -385,7 +384,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
.views()
.filter(v => v)
.map(dv => dv!.rootDoc);
- TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+ TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinData: { dataview: e.altKey }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
e.stopPropagation();
}}>
<div className="documentButtonBar-pinTypes">
@@ -416,7 +415,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
<Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
- <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
+ <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={this.openContextMenu}>
<FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
</div>
</Tooltip>
@@ -518,7 +517,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
)
}>
<div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}>
- {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />
</div>
</Flyout>
</div>
@@ -569,7 +568,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc;
const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
if (trail !== anchor.presTrail) {
- DocUtils.MakeLink({ doc: anchor }, { doc: trail }, 'link trail');
+ DocUtils.MakeLink(anchor, trail, { linkRelationship: 'link trail' });
anchor.presTrail = trail;
}
Doc.ActivePresentation = trail;
@@ -594,7 +593,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<LinkPopup
key="popup"
showPopup={this._showLinkPopup}
- linkCreated={link => (link.linkDisplay = !this.props.views().lastElement()?.rootDoc.isLinkButton)}
+ linkCreated={link => (link.linkDisplay = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))}
linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)}
linkFrom={() => this.props.views().lastElement()?.rootDoc}
/>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2982f8a99..9ffbe083f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -31,6 +31,8 @@ import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { ImageBox } from './nodes/ImageBox';
import React = require('react');
+import { RichTextField } from '../../fields/RichTextField';
+import { LinkFollower } from '../util/LinkFollower';
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -67,8 +69,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
reaction(
() => SelectionManager.Views().slice(),
action(docs => {
- this._showNothing = true;
- docs.length > 1 && (this._showNothing = false); // show decorations if multiple docs are selected
+ this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing
this._editingTitle = false;
})
);
@@ -83,10 +84,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
);
}
- @observable overrideBounds = false;
@computed
get Bounds() {
- if (this.overrideBounds) return { x: 0, y: 0, r: 0, b: 0 };
+ if (LinkFollower.IsFollowing) return { x: 0, y: 0, r: 0, b: 0 };
const views = SelectionManager.Views();
return views
.filter(dv => dv.props.renderDepth > 0)
@@ -128,19 +128,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
//@ts-ignore
const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
- Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
- if (d.rootDoc.syncLayoutFieldWithTitle) {
- const title = titleField.toString();
+ if (titleField.toString().startsWith('<this>')) {
+ const title = titleField.toString().replace(/<this>\.?/, '');
const curKey = Doc.LayoutFieldKey(d.rootDoc);
- if (curKey !== title && d.dataDoc[title] === undefined) {
- d.rootDoc.layout = FormattedTextBox.LayoutString(title);
- setTimeout(() => {
- const val = d.dataDoc[curKey];
- d.dataDoc[curKey] = undefined;
- d.dataDoc[title] = val;
- });
+ if (curKey !== title) {
+ if (title) {
+ if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') {
+ d.rootDoc.layoutKey = `layout_${title}`;
+ d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title);
+ d.rootDoc[`${title}-nativeWidth`] = d.rootDoc[`${title}-nativeHeight`] = 0;
+ }
+ } else {
+ d.rootDoc.layoutKey = undefined;
+ }
}
+ } else {
+ Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
}
}),
'title blur'
@@ -454,17 +458,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerDown = (e: React.PointerEvent): void => {
- const views = SelectionManager.Views().map(dv => dv.rootDoc);
- this._inkDragDocs = views
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => {
- if (InkStrokeProperties.Instance._lock) {
- Doc.SetNativeHeight(doc, NumCast(doc._height));
- Doc.SetNativeWidth(doc, NumCast(doc._width));
- }
- return { doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) };
- });
-
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
this._resizeHdlId = e.currentTarget.className;
@@ -488,10 +481,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (!first) return false;
let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
var fixedAspect = Doc.NativeAspect(first.layoutDoc);
- InkStrokeProperties.Instance._lock &&
- SelectionManager.Views()
- .filter(dv => dv.rootDoc.type === DocumentType.INK)
- .forEach(dv => (fixedAspect = Doc.NativeAspect(dv.rootDoc)));
const resizeHdl = this._resizeHdlId.split(' ')[0];
if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) {
@@ -917,9 +906,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
pointerEvents: 'none',
}}>
{this._isRotating ? null : (
- <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
- <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
- </div>
+ <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}>
+ <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+ <IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} />
+ </div>
+ </Tooltip>
)}
</div>
{!this._showRotCenter ? null : (
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index bb190e93b..164b6c57a 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -129,7 +129,7 @@ export class EditableView extends React.Component<EditableProps> {
this._editing = true;
this.props.isEditingCallback?.(true);
}
- e.stopPropagation();
+ // e.stopPropagation();
}
};
@@ -215,7 +215,7 @@ export class EditableView extends React.Component<EditableProps> {
className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`}
ref={this._ref}
style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }}
- onPointerDown={e => e.stopPropagation()}
+ //onPointerDown={this.stopPropagation}
onClick={this.onClick}
placeholder={this.props.placeholder}>
<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 3fbf6e445..0f8b46dbe 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -372,7 +372,7 @@ export class KeyManager {
list.push(doc);
}
if (count === docids.length) {
- const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d)).clone : d)));
+ const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d)));
if (added.length) {
added.map(doc => (doc.context = targetDataDoc));
undoBatch(() => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 1d8d52425..e6df0801c 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -19,7 +19,6 @@ export class InkStrokeProperties {
return this._Instance || new InkStrokeProperties();
}
- @observable _lock = false;
@observable _controlButton = false;
@observable _currentPoint = -1;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 4f08a8e22..2fd6cc4d6 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,10 +25,11 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
import { InkData, InkField } from '../../fields/InkField';
-import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
-import { DashColor, OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
+import { Docs } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
@@ -41,13 +42,12 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
import './InkStroke.scss';
import { InkStrokeProperties } from './InkStrokeProperties';
import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView, DocFocusOptions, DocumentView } from './nodes/DocumentView';
+import { DocComponentView } from './nodes/DocumentView';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { PinProps, PresBox } from './nodes/trails';
import { StyleProp } from './StyleProvider';
import Color = require('color');
-import { Docs } from '../documents/Documents';
-import { PinProps, PresBox } from './nodes/trails';
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -179,7 +179,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
}),
action((e: PointerEvent, doubleTap: boolean | undefined) => {
- doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
if (doubleTap) {
InkStrokeProperties.Instance._controlButton = true;
InkStrokeProperties.Instance._currentPoint = -1;
@@ -418,7 +417,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
inkScaleX,
inkScaleY,
'',
- this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'),
+ this.props.pointerEvents?.() ?? 'visiblepainted',
0.0,
false,
downHdlr,
@@ -465,7 +464,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
//top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
}}>
<FormattedTextBox
- {...OmitKeys(this.props, ['children']).omit}
+ {...this.props}
+ setHeight={undefined}
setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
yPadding={10}
xPadding={10}
@@ -474,7 +474,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
dontRegisterView={true}
noSidebar={true}
dontScale={true}
- isContentActive={this.isContentActive}
+ isContentActive={this.props.isContentActive}
/>
</div>
)}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 31888cbb4..16d2145ac 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -10,7 +10,7 @@ import 'normalize.css';
import * as React from 'react';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { ScriptField } from '../../fields/ScriptField';
-import { DocCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
@@ -40,7 +40,7 @@ import { DashboardView } from './DashboardView';
import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
-import { DASHBOARD_SELECTOR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
+import { TOPBAR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
import { Colors } from './global/globalEnums';
import { KeyManager } from './GlobalKeyHandler';
import { InkTranscription } from './InkTranscription';
@@ -57,7 +57,6 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
import { RadialMenu } from './nodes/RadialMenu';
import { TaskCompletionBox } from './nodes/TaskCompletedBox';
-import { PresBox } from './nodes/trails';
import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
import { PreviewCursor } from './PreviewCursor';
@@ -87,7 +86,7 @@ export class MainView extends React.Component {
return this._hideUI ? 0 : 27;
} // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
@computed private get topOfDashUI() {
- return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
+ return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
}
@computed private get topOfHeaderBarDoc() {
return this.topOfDashUI;
@@ -215,6 +214,8 @@ export class MainView extends React.Component {
window.removeEventListener('keyup', KeyManager.Instance.unhandle);
window.removeEventListener('keydown', KeyManager.Instance.handle);
window.removeEventListener('pointerdown', this.globalPointerDown, true);
+ window.removeEventListener('pointermove', this.globalPointerMove, true);
+ window.removeEventListener('mouseclick', this.globalPointerClick, true);
window.removeEventListener('paste', KeyManager.Instance.paste as any);
document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
}
@@ -259,6 +260,7 @@ export class MainView extends React.Component {
fa.faConciergeBell,
fa.faWindowRestore,
fa.faFolder,
+ fa.faFolderOpen,
fa.faMapPin,
fa.faMapMarker,
fa.faFingerprint,
@@ -269,6 +271,7 @@ export class MainView extends React.Component {
fa.faLaptopCode,
fa.faMale,
fa.faCopy,
+ fa.faHome,
fa.faHandPointLeft,
fa.faHandPointRight,
fa.faCompass,
@@ -481,11 +484,28 @@ export class MainView extends React.Component {
fa.faHighlighter,
fa.faRemoveFormat,
fa.faHandPointUp,
+ fa.faXRay,
+ fa.faZ,
+ fa.faArrowsUpToLine,
+ fa.faArrowsDownToLine,
]
);
}
+ private longPressTimer: NodeJS.Timeout | undefined;
+ globalPointerClick = action((e: any) => {
+ this.longPressTimer && clearTimeout(this.longPressTimer);
+ DocumentView.LongPress = false;
+ });
+ globalPointerMove = action((e: PointerEvent) => {
+ if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer);
+ });
globalPointerDown = action((e: PointerEvent) => {
+ DocumentView.LongPress = false;
+ this.longPressTimer = setTimeout(
+ action(() => (DocumentView.LongPress = true)),
+ 1000
+ );
DocumentManager.removeOverlayViews();
Doc.linkFollowUnhighlight();
AudioBox.Enabled = true;
@@ -506,6 +526,8 @@ export class MainView extends React.Component {
window.addEventListener('dragover', e => e.preventDefault(), false);
// document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined));
document.addEventListener('pointerdown', this.globalPointerDown, true);
+ document.addEventListener('pointermove', this.globalPointerMove, true);
+ document.addEventListener('mouseclick', this.globalPointerClick, true);
document.addEventListener(
'click',
(e: MouseEvent) => {
@@ -549,6 +571,7 @@ export class MainView extends React.Component {
@computed get exploreMode() {
return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
}
+ waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined);
headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
@computed get headerBarDocView() {
@@ -669,7 +692,8 @@ export class MainView extends React.Component {
mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
static addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
- const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
+ const keyValue = whereFields[1]?.includes('KeyValue');
+ const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
if (doc.dockingConfig) return DashboardView.openDashboard(doc);
// prettier-ignore
switch (whereFields[0]) {
@@ -678,7 +702,7 @@ export class MainView extends React.Component {
case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods);
case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods);
- case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods);
+ case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue);
}
};
@@ -854,7 +878,7 @@ export class MainView extends React.Component {
@computed get docButtons() {
return !Doc.MyDockedBtns ? null : (
- <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !Doc.MyDockedBtns.linearViewIsExpanded ? '42px' : undefined }}>
+ <div className="mainView-docButtons" ref={this._docBtnRef}>
<CollectionLinearView
Document={Doc.MyDockedBtns}
DataDoc={undefined}
@@ -892,7 +916,7 @@ export class MainView extends React.Component {
);
}
@computed get snapLines() {
- return !SnappingManager.GetShowSnapLines() ? null : (
+ return !SelectionManager.Views().some(dv => dv.rootDoc.showSnapLines) ? null : (
<div className="mainView-snapLines">
<svg style={{ width: '100%', height: '100%' }}>
{SnappingManager.horizSnapLines().map(l => (
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c02435518..8fd2b87cc 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -14,6 +14,8 @@ import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { AnchorMenu } from './pdf/AnchorMenu';
import React = require('react');
+import { ScriptField } from '../../fields/ScriptField';
+import { FollowLinkScript } from '../util/LinkFollower';
const _global = (window /* browser */ || global) /* node */ as any;
export interface MarqueeAnnotatorProps {
@@ -146,7 +148,12 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
const scale = this.props.scaling?.() || 1;
const anno = savedAnnos[0];
const containerOffset = this.props.containerOffset?.() || [0, 0];
- const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: 'Annotation on ' + this.props.rootDoc.title });
+ const marqueeAnno = Docs.Create.FreeformDocument([], {
+ onClick: isLinkButton ? FollowLinkScript() : undefined,
+ backgroundColor: color,
+ annotationOn: this.props.rootDoc,
+ title: 'Annotation on ' + this.props.rootDoc.title,
+ });
marqueeAnno.x = NumCast(this.props.docView.props.Document.panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1);
marqueeAnno.y = NumCast(this.props.docView.props.Document.panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1) + NumCast(this.props.scrollTop);
marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._viewScale, 1);
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 34e8cd6dd..ec22128d4 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -219,6 +219,7 @@ export class OverlayView extends React.Component {
PanelHeight={d[HeightSym]}
ScreenToLocalTransform={this.docScreenToLocalXf(d)}
renderDepth={1}
+ hideDecorations={true}
isDocumentActive={returnTrue}
isContentActive={returnTrue}
whenChildContentsActiveChanged={emptyFunction}
@@ -252,8 +253,3 @@ export class OverlayView extends React.Component {
);
}
}
-// bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
-ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
- OverlayView.Instance.addWindow(<ScriptingRepl />, options);
- addOverlayWindow('ScriptingRepl', { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
-});
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 3712fff58..95ae65d7a 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -81,14 +81,8 @@ export class PreviewCursor extends React.Component<{}> {
const batch = UndoManager.StartBatch('cloning');
{
- const docs = await Promise.all(
- docids
- .filter((did, i) => i)
- .map(async did => {
- const doc = Cast(await DocServer.GetRefField(did), Doc, null);
- return clone ? (await Doc.MakeClone(doc)).clone : doc;
- })
- );
+ const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null)));
+ const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy;
const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0;
const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0;
docs.map(doc => {
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ebbe20077..031d501ad 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -21,6 +21,7 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView';
import { pasteImageBitmap } from './nodes/WebBoxRenderer';
import './PropertiesButtons.scss';
import React = require('react');
+import { IsFollowLinkScript } from '../util/LinkFollower';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -133,7 +134,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
const containerDoc = dv.rootDoc;
//containerDoc.followAllLinks =
// containerDoc.noShadow =
- // containerDoc.noHighlighting =
+ // containerDoc.disableDocBrushing =
// containerDoc._forceActive =
containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox;
containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
@@ -289,6 +290,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
.filter(dv => dv.docView)
.map(dv => dv.docView!)
.forEach(docView => {
+ const linkButton = IsFollowLinkScript(docView.props.Document.onClick);
docView.noOnClick();
switch (onClick) {
case 'enterPortal':
@@ -299,11 +301,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
break;
case 'linkInPlace':
docView.toggleFollowLink(false, false);
- docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.lightbox : undefined;
+ docView.props.Document.followLinkLocation = linkButton ? OpenWhere.lightbox : undefined;
break;
case 'linkOnRight':
docView.toggleFollowLink(false, false);
- docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.addRight : undefined;
+ docView.props.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined;
break;
}
});
@@ -326,7 +328,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
];
const list = buttonList.map(value => {
const click = () => this.handleOptionChange(value[0]);
- const linkButton = BoolCast(this.selectedDoc._isLinkButton);
+ const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
const followLoc = this.selectedDoc._followLinkLocation;
const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 03b4100a7..fbc7d7696 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -102,7 +102,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable inOptions: boolean = false;
@observable _controlButton: boolean = false;
- @observable _lock: boolean = false;
componentDidMount() {
this.selectedDocListenerDisposer?.();
@@ -586,16 +585,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
</div>
</Tooltip>
- <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? 'Unlock ratio' : 'Lock ratio'}</div>}>
- <div className="inking-button-lock" onPointerDown={action(() => (InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock))}>
- <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? 'lock' : 'unlock'} color="white" size="lg" />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Rotate 90Ëš'}</div>}>
- <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}>
- <FontAwesomeIcon icon="undo" color="white" size="lg" />
- </div>
- </Tooltip>
</div>
);
}
@@ -644,9 +633,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@action
upDownButtons = (dirs: string, field: string) => {
switch (field) {
- case 'rot':
- this.rotate(dirs === 'up' ? 0.1 : -0.1);
- break;
case 'Xps':
this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10));
break;
@@ -662,7 +648,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const oldX = NumCast(this.selectedDoc?.x);
const oldY = NumCast(this.selectedDoc?.y);
this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth) * NumCast(this.selectedDoc?._height));
const doc = this.selectedDoc;
if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
const ink = Cast(doc.data, InkField)?.inkData;
@@ -684,7 +669,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const oX = NumCast(this.selectedDoc?.x);
const oY = NumCast(this.selectedDoc?.y);
this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight) * NumCast(this.selectedDoc?._width));
const docu = this.selectedDoc;
if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
const ink = Cast(docu.data, InkField)?.inkData;
@@ -704,11 +688,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
getField(key: string) {
- //if (this.selectedDoc) {
return Field.toString(this.selectedDoc?.[key] as Field);
- // } else {
- // return undefined as Opt<string>;
- // }
}
@computed get shapeXps() {
@@ -717,9 +697,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get shapeYps() {
return this.getField('y');
}
- @computed get shapeRot() {
- return this.getField('rotation');
- }
@computed get shapeHgt() {
return this.getField('_height');
}
@@ -732,18 +709,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
set shapeYps(value) {
this.selectedDoc && (this.selectedDoc.y = Number(value));
}
- set shapeRot(value) {
- this.selectedDoc && (this.selectedDoc.rotation = Number(value));
- }
set shapeWid(value) {
- const oldWidth = NumCast(this.selectedDoc?._width);
this.selectedDoc && (this.selectedDoc._width = Number(value));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
}
set shapeHgt(value) {
- const oldHeight = NumCast(this.selectedDoc?._height);
this.selectedDoc && (this.selectedDoc._height = Number(value));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
}
@computed get hgtInput() {
@@ -790,30 +760,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
'Y:'
);
}
- @computed get rotInput() {
- return this.inputBoxDuo(
- 'rot',
- this.shapeRot,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.rotate(Number(val) - Number(this.shapeRot));
- this.shapeRot = val;
- }
- return true;
- },
- '∠:',
- 'rot',
- this.shapeRot,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.rotate(Number(val) - Number(this.shapeRot));
- this.shapeRot = val;
- }
- return true;
- },
- ''
- );
- }
@observable private _fillBtn = false;
@observable private _lineBtn = false;
@@ -1080,10 +1026,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get transformEditor() {
return (
<div className="transform-editor">
- {this.controlPointsButton}
+ {this.isInk ? this.controlPointsButton : null}
{this.hgtInput}
{this.XpsInput}
- {this.rotInput}
</div>
);
}
@@ -1194,17 +1139,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
)}
- {this.isInk ? (
- <div className="propertiesView-transform">
- <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
- Transform
- <div className="propertiesView-transform-title-icon">
- <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
+ <div className="propertiesView-transform">
+ <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
+ Transform
+ <div className="propertiesView-transform-title-icon">
+ <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
</div>
- {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
</div>
- ) : null}
+ {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
+ </div>
</>
);
}
@@ -1678,8 +1621,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-input inline">
<p>Center Target (no zoom)</p>
<button
- style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))}
+ style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
onClick={e => e.stopPropagation()}
className="propertiesButton">
<FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
@@ -1687,7 +1630,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
<div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
<p>Zoom %</p>
- <div className="ribbon-property" style={{ display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'inline-flex' }}>
+ <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
<input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
<div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
@@ -1706,11 +1649,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
</button>
</div>
- {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
+ {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
<div
className={'slider-headers'}
style={{
- display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid',
+ display: !targZoom ? 'none' : 'grid',
justifyContent: 'space-between',
width: `calc(100% - ${indent * 2}px)`,
marginLeft: indent,
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 7519cbb05..2d2b0f83e 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -5,7 +5,7 @@ import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
import { DocCast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils';
+import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZero } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { LinkManager } from '../util/LinkManager';
@@ -26,6 +26,7 @@ interface ExtraProps {
// usePanelWidth: boolean;
showSidebar: boolean;
nativeWidth: number;
+ usePanelWidth?: boolean;
whenChildContentsActiveChanged: (isActive: boolean) => void;
ScreenToLocalTransform: () => Transform;
sidebarAddDocument: (doc: Doc | Doc[], suffix: string) => boolean;
@@ -73,7 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
});
FormattedTextBox.SelectOnLoad = target[Id];
FormattedTextBox.DontSelectInitialText = true;
- const link = DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+ const link = DocUtils.MakeLink(anchor, target, { linkRelationship: 'inline comment:comment on' });
link && (link.linkDisplay = false);
const taggedContent = this.docFilters()
@@ -84,7 +85,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
Doc.GetProto(target)[key] = val;
return {
type: 'dashField',
- attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
+ attrs: { fieldKey: key, docId: '', hideKey: false, editable: true },
marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
};
});
@@ -111,7 +112,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
{ type: 'pFontSize', attrs: { fontSize: '8px' } },
{ type: 'em' },
],
- attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false },
+ attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false },
},
],
},
@@ -158,13 +159,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
.ScreenToLocalTransform()
.translate(Doc.NativeWidth(this.props.dataDoc), 0)
.scale(this.props.NativeDimScaling?.() || 1);
- // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 :
- // this.props.usePanelWidth ? this.props.PanelWidth() :
- // (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
panelWidth = () =>
!this.props.showSidebar
? 0
- : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP
+ : this.props.usePanelWidth // [DocumentType.RTF, DocumentType.MAP].includes(this.props.layoutDoc.type as any)
? this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1)
: ((NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth()) / NumCast(this.props.nativeWidth);
panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
@@ -224,20 +222,21 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
</div>
<div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
<CollectionStackingView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- ref={this._stackRef}
+ {...this.props}
+ setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
+ ref={this._stackRef}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
docFilters={this.docFilters}
- scaleField={this.sidebarKey + '-scale'}
sortFunc={this.sortByLinkAnchorY}
setHeight={this.setHeightCallback}
isAnnotationOverlay={false}
select={emptyFunction}
NativeDimScaling={returnOne}
//childShowTitle={this.showTitle}
+ isAnyChildContentActive={returnFalse}
childDocumentsActive={this.props.isContentActive}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
childHideDecorationTitle={returnTrue}
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index b1c97164a..80c878386 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -1,20 +1,31 @@
+.styleProvider-filter,
+.styleProvider-audio,
.styleProvider-lock {
font-size: 10;
width: 15;
height: 15;
position: absolute;
- right: -0;
+ right: -15;
top: 0;
background: black;
pointer-events: all;
opacity: 0.3;
display: flex;
+ flex-direction: column;
color: gold;
border-radius: 3px;
justify-content: center;
cursor: default;
}
-.styleProvider-lock:hover {
+.styleProvider-filter {
+ right: 0;
+}
+.styleProvider-audio {
+ right: 15;
+}
+.styleProvider-lock:hover,
+.styleProvider-audio:hover,
+.styleProvider-filter:hover {
opacity: 1;
}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index ce764c7bf..b950b4860 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,16 +1,18 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { Shadows } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { extname } from 'path';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../fields/Doc';
import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types';
-import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
+import { DashColor, lightOrDark, Utils } from '../../Utils';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocFocusOrOpen } from '../util/DocumentManager';
+import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager';
+import { IsFollowLinkScript } from '../util/LinkFollower';
import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
-import { ColorScheme } from '../util/SettingsManager';
+import { ColorScheme, SettingsManager } from '../util/SettingsManager';
import { undoBatch, UndoManager } from '../util/UndoManager';
import { TreeSort } from './collections/TreeView';
import { Colors } from './global/globalEnums';
@@ -18,6 +20,7 @@ import { InkingStroke } from './InkingStroke';
import { MainView } from './MainView';
import { DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
+import { KeyValueBox } from './nodes/KeyValueBox';
import { SliderBox } from './nodes/SliderBox';
import './StyleProvider.scss';
import React = require('react');
@@ -35,10 +38,10 @@ export enum StyleProp {
FillColor = 'fillColor', // fill color of an ink stroke or shape
WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note
HideLinkButton = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button
- LinkSource = 'linkSource', // source document of a link -- used by LinkAnchorBox
PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified
Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background
HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that
+ ShowCaption = 'showCaption',
TitleHeight = 'titleHeight', // Height of Title area
ShowTitle = 'showTitle', // whether to display a title on a Document (optional :hover suffix)
JitterRotation = 'jitterRotation', // whether documents should be randomly rotated
@@ -88,8 +91,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const isAnchor = property.includes(':anchor');
const isAnnotated = property.includes(':annotated');
const isOpen = property.includes(':open');
+ const boxBackground = property.includes(':box');
const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : '';
- const isBackground = () => doc && BoolCast(doc._lockedPosition);
+ const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
@@ -112,16 +116,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
return allSorts;
case StyleProp.Highlighting:
- if (doc && !doc.noHighlighting) {
- const highlightIndex = Doc.isBrushedHighlightedDegree(doc);
- const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+ if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) {
+ const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc);
+ const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
+ const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
- const excludeTypes = [DocumentType.FONTICON];
- let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
- if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') {
+ if (highlightIndex) {
return {
highlightStyle,
- highlightColor: highlightIndex !== Doc.DocBrushStatus.highlighted && SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor,
+ highlightColor,
highlightIndex,
highlightStroke: doc.type === DocumentType.INK,
};
@@ -132,10 +135,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return undefined;
case StyleProp.WidgetColor:
return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
- // return doc = dragManager.dragDocument ? props.dragEffects.opacity??CastofOpacityonline94 : Cast())
- // same idea for background Color
case StyleProp.Opacity:
- return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
+ return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
case StyleProp.HideLinkButton:
return props?.hideLinkButton || (!selected && doc?.hideLinkButton);
case StyleProp.FontSize:
@@ -148,6 +149,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return (
(doc &&
!doc.presentationTargetDoc &&
+ !props?.LayoutTemplateString?.includes(KeyValueBox.name) &&
props?.showTitle?.() !== '' &&
StrCast(
doc._showTitle,
@@ -167,21 +169,35 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
if (docColor) return docColor;
const docView = props?.DocumentView?.();
- const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, 'backgroundColor');
+ const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, StyleProp.BackgroundColor);
if (!backColor) return undefined;
return lightOrDark(backColor);
case StyleProp.Hidden:
- return BoolCast(doc?.hidden);
+ return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? false : BoolCast(doc?.hidden);
case StyleProp.BorderRounding:
return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : ''));
case StyleProp.TitleHeight:
return 15;
case StyleProp.BorderPath:
- return Doc.IsComicStyle(doc) && props?.renderDepth && doc?.type !== DocumentType.INK
- ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }
- : { path: undefined, width: 0 };
+ const borderPath = Doc.IsComicStyle(doc) &&
+ props?.renderDepth &&
+ doc?.type !== DocumentType.INK && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 };
+ return !borderPath
+ ? null
+ : {
+ clipPath: `path('${borderPath.path}')`,
+ jsx: (
+ <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
+ <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}>
+ <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
+ </svg>
+ </div>
+ ),
+ };
case StyleProp.JitterRotation:
return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
+ case StyleProp.ShowCaption:
+ return doc?._viewType === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._showCaption);
case StyleProp.HeaderMargin:
return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
(doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) ||
@@ -201,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
docColor = docColor || (darkScheme() ? 'transparent' : 'transparent');
break;
case DocumentType.FONTICON:
- docColor = docColor || Colors.DARK_GRAY;
+ docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY;
break;
case DocumentType.RTF:
docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
@@ -218,7 +234,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
docColor = docColor || 'transparent';
break;
case DocumentType.LABEL:
- docColor = docColor || (doc.annotationOn !== undefined ? 'rgba(128, 128, 128, 0.18)' : undefined) || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
+ docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
break;
case DocumentType.BUTTON:
docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
@@ -262,14 +278,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.BoxShadow: {
if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
- if (doc?.isLinkButton && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+ if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
switch (doc?.type) {
case DocumentType.COL:
return StrCast(
doc?.boxShadow,
doc?._viewType === CollectionViewType.Pile
? '4px 4px 10px 2px'
- : isBackground() || doc?._isGroup || docProps?.LayoutTemplateString
+ : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString
? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
: `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, '0.2vw 0.2vw 0.8vw')}`
);
@@ -280,30 +296,67 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return doc.z
? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
: props?.ContainingCollectionDoc?._useClusters
- ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
: NumCast(doc.group, -1) !== -1
- ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
- : isBackground()
+ ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ : lockedPosition()
? undefined // if it's a background & has a cluster color, make the shadow spread really big
: StrCast(doc.boxShadow, '');
}
}
case StyleProp.PointerEvents:
- if (MainView.Instance._exploreMode) return 'all';
+ const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
+ if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
+ if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
+ if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
if (props?.pointerEvents?.() === 'none') return 'none';
- const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
- if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none';
- if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
- return undefined;
+ if (opacity() === 0 || doc?.isInkMask) return 'none';
+ if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all';
+ return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
case StyleProp.Decorations:
- if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
- return doc && doc.pointerEvents === 'none' && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
- <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
- <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" />
+ const lock = () => {
+ if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform) {
+ return doc?.pointerEvents !== 'none' ? null : (
+ <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
+ <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" />
+ </div>
+ );
+ }
+ };
+ const filter = () => {
+ const showFilterIcon =
+ StrListCast(doc?._docFilters).length || StrListCast(doc?._docRangeFilters).length
+ ? '#18c718bd' //'hasFilter'
+ : docProps?.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.docRangeFilters().length
+ ? 'orange' //'inheritsFilter'
+ : undefined;
+ return !showFilterIcon ? null : (
+ <div className="styleProvider-filter" onClick={action(() => (SettingsManager.propertiesWidth = 250))}>
+ <FontAwesomeIcon icon={'filter'} size="lg" style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: showFilterIcon, zIndex: 1 }} />
</div>
- ) : null;
- }
+ );
+ };
+ const audio = () => {
+ const audioAnnoState = (doc: Doc) => StrCast(doc.audioAnnoState, 'stopped');
+ const audioAnnosCount = (doc: Doc) => StrListCast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations']).length;
+ if (!doc || props?.renderDepth === -1 || (!audioAnnosCount(doc) && audioAnnoState(doc) === 'stopped')) return null;
+ const audioIconColors: { [key: string]: string } = { recording: 'red', playing: 'green', stopped: 'blue' };
+ return (
+ <Tooltip title={<div>{StrListCast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations-text']).lastElement()}</div>}>
+ <div className="styleProvider-audio" onPointerDown={() => DocumentManager.Instance.getFirstDocumentView(doc)?.docView?.playAnnotation()}>
+ <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors[audioAnnoState(doc)] }} icon={'file-audio'} size="sm" />
+ </div>
+ </Tooltip>
+ );
+ };
+ return (
+ <>
+ {lock()}
+ {filter()}
+ {audio()}
+ </>
+ );
}
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 863829a51..45db240a9 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,7 +1,6 @@
import { action, computed, observable, ObservableSet, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast } from '../../fields/Doc';
-import { List } from '../../fields/List';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
@@ -80,7 +79,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
};
componentDidMount() {
!this._addedKeys && (this._addedKeys = new ObservableSet());
- Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document)))
+ [...Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))), ...Array.from(Object.keys(this.props.docViews[0].props.Document))]
.filter(key => key.startsWith('layout_'))
.map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
}
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 21a5af83f..3465a5283 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -345,14 +345,6 @@ export class Keyframe extends React.Component<IProps> {
}),
TimelineMenu.Instance.addItem(
'button',
- 'Show Data',
- action(() => {
- const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
- CollectionDockingView.AddSplit(kvp, OpenWhereMod.right);
- })
- ),
- TimelineMenu.Instance.addItem(
- 'button',
'Delete',
action(() => {
(this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 01f41869e..a266c9207 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -5,11 +5,11 @@ import * as React from 'react';
import { Doc } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse, Utils } from '../../../Utils';
+import { returnFalse, returnZero, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { DocumentView } from '../nodes/DocumentView';
import { StyleProp } from '../StyleProvider';
-import "./CollectionCarousel3DView.scss";
+import './CollectionCarousel3DView.scss';
import { CollectionSubView } from './CollectionSubView';
@observer
@@ -20,134 +20,146 @@ export class CollectionCarousel3DView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- componentWillUnmount() { this._dropDisposer?.(); }
+ componentWillUnmount() {
+ this._dropDisposer?.();
+ }
- protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+ //used for stacking and masonry view
this._dropDisposer?.();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
- }
+ };
panelWidth = () => this.props.PanelWidth() / 3;
panelHeight = () => this.props.PanelHeight() * 0.6;
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
+ isChildContentActive = () => (this.isContentActive() ? true : false);
+
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._itemIndex);
- const displayDoc = (childPair: { layout: Doc, data: Doc }) => {
- return <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
- onDoubleClick={this.onChildDoubleClick}
- renderDepth={this.props.renderDepth + 1}
- LayoutTemplate={this.props.childLayoutTemplate}
- LayoutTemplateString={this.props.childLayoutString}
- Document={childPair.layout}
- DataDoc={childPair.data}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- bringToFront={returnFalse}
- />;
+ const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
+ return (
+ <DocumentView
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ suppressSetHeight={true}
+ onDoubleClick={this.onChildDoubleClick}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplate={this.props.childLayoutTemplate}
+ LayoutTemplateString={this.props.childLayoutString}
+ Document={childPair.layout}
+ DataDoc={childPair.data}
+ isContentActive={this.isChildContentActive}
+ isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ bringToFront={returnFalse}
+ />
+ );
};
- return (this.childLayoutPairs.map((childPair, index) => {
+ return this.childLayoutPairs.map((childPair, index) => {
return (
- <div key={childPair.layout[Id]}
- className={`collectionCarousel3DView-item${index === currentIndex ? "-active" : ""} ${index}`}
- style={index === currentIndex ?
- { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } :
- { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
+ <div
+ key={childPair.layout[Id]}
+ className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`}
+ style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
{displayDoc(childPair)}
- </div>);
- }));
+ </div>
+ );
+ });
}
changeSlide = (direction: number) => {
this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
- }
+ };
onArrowClick = (e: React.MouseEvent, direction: number) => {
e.stopPropagation();
this.changeSlide(direction);
- !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = (direction === 1) ? "fwd" : "back"); // while autoscroll is on, keep the other autoscroll button hidden
+ !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = direction === 1 ? 'fwd' : 'back'); // while autoscroll is on, keep the other autoscroll button hidden
!this.layoutDoc.autoScrollOn && this.fadeScrollButton(); // keep pause button visible while autoscroll is on
- }
+ };
interval?: number;
startAutoScroll = (direction: number) => {
this.interval = window.setInterval(() => {
this.changeSlide(direction);
}, this.scrollSpeed);
- }
+ };
stopAutoScroll = () => {
window.clearInterval(this.interval);
this.interval = undefined;
this.fadeScrollButton();
- }
+ };
toggleAutoScroll = (direction: number) => {
this.layoutDoc.autoScrollOn = this.layoutDoc.autoScrollOn ? false : true;
this.layoutDoc.autoScrollOn ? this.startAutoScroll(direction) : this.stopAutoScroll();
- }
+ };
fadeScrollButton = () => {
window.setTimeout(() => {
- !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = "none"); //fade away after 1.5s if it's not clicked.
+ !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = 'none'); //fade away after 1.5s if it's not clicked.
}, 1500);
- }
+ };
@computed get buttons() {
if (!this.props.isContentActive()) return null;
- return <div className="arrow-buttons" >
- <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={(e) => this.onArrowClick(e, -1)}
- >
- <FontAwesomeIcon icon={"angle-left"} size={"2x"} />
- </div>
- <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={(e) => this.onArrowClick(e, 1)}
- >
- <FontAwesomeIcon icon={"angle-right"} size={"2x"} />
+ return (
+ <div className="arrow-buttons">
+ <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}>
+ <FontAwesomeIcon icon={'angle-left'} size={'2x'} />
+ </div>
+ <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}>
+ <FontAwesomeIcon icon={'angle-right'} size={'2x'} />
+ </div>
+ {this.autoScrollButton}
</div>
- {this.autoScrollButton}
- </div>;
+ );
}
@computed get autoScrollButton() {
const whichButton = this.layoutDoc.showScrollButton;
- return <>
- <div className={`carousel3DView-back-scroll${whichButton === "back" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={() => this.toggleAutoScroll(-1)}>
- {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-left"} size={"1x"} />}
- </div>
- <div className={`carousel3DView-fwd-scroll${whichButton === "fwd" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={() => this.toggleAutoScroll(1)}>
- {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-right"} size={"1x"} />}
- </div>
- </>;
+ return (
+ <>
+ <div className={`carousel3DView-back-scroll${whichButton === 'back' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(-1)}>
+ {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-left'} size={'1x'} />}
+ </div>
+ <div className={`carousel3DView-fwd-scroll${whichButton === 'fwd' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(1)}>
+ {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-right'} size={'1x'} />}
+ </div>
+ </>
+ );
}
@computed get dots() {
- return (this.childLayoutPairs.map((_child, index) =>
- <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? "-active" : ""}`}
- onClick={() => this.layoutDoc._itemIndex = index} />));
+ return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />);
}
render() {
const index = NumCast(this.layoutDoc._itemIndex);
const translateX = this.panelWidth() * (1 - index);
- return <div className="collectionCarousel3DView-outer" ref={this.createDashEventsTarget}
- style={{
- background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
- }} >
- <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
- {this.content}
- </div>
- {this.props.Document._chromeHidden ? (null) : this.buttons}
- <div className="dot-bar">
- {this.dots}
+ return (
+ <div
+ className="collectionCarousel3DView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+ color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
+ }}>
+ <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
+ {this.content}
+ </div>
+ {this.props.Document._chromeHidden ? null : this.buttons}
+ <div className="dot-bar">{this.dots}</div>
</div>
- </div>;
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 32f6207ed..c0220f804 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, Opt } from '../../../fields/Doc';
import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -47,7 +47,7 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
const curDoc = this.childLayoutPairs?.[index];
- const captionProps = { ...OmitKeys(this.props, ['setHeight']).omit, fieldKey: 'caption' };
+ const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
const marginX = NumCast(this.layoutDoc['caption-xMargin']);
const marginY = NumCast(this.layoutDoc['caption-yMargin']);
const showCaptions = StrCast(this.layoutDoc._showCaption);
@@ -55,7 +55,9 @@ export class CollectionCarouselView extends CollectionSubView() {
<>
<div className="collectionCarouselView-image" key="image">
<DocumentView
- {...OmitKeys(this.props, ['setHeight', 'NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
hideCaptions={showCaptions ? true : false}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 9b6554d67..4d000542c 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -28,13 +28,12 @@ import React = require('react');
import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
import { OverlayView } from '../OverlayView';
import { ScriptingRepl } from '../ScriptingRepl';
-import { ScriptField } from '../../../fields/ScriptField';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
export class CollectionDockingView extends CollectionSubView() {
@observable public static Instance: CollectionDockingView | undefined;
- public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) {
+ public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
@@ -42,6 +41,7 @@ export class CollectionDockingView extends CollectionSubView() {
width: width,
props: {
documentId: document[Id],
+ keyValue,
panelName, // name of tab that can be used to close or replace its contents
},
};
@@ -146,10 +146,10 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
@action
- public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean {
+ public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean, keyValue?: boolean): boolean {
const instance = CollectionDockingView.Instance;
if (!instance) return false;
- const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
+ const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
if (!panelName && stack) {
const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config);
const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout);
@@ -171,10 +171,10 @@ export class CollectionDockingView extends CollectionSubView() {
}
@undoBatch
- public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) {
+ public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1
? CollectionDockingView.CloseSplit(doc)
- : CollectionDockingView.AddSplit(doc, location, stack, panelName);
+ : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue);
}
//
@@ -182,10 +182,10 @@ export class CollectionDockingView extends CollectionSubView() {
//
@undoBatch
@action
- public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) {
+ public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
if (!CollectionDockingView.Instance) return false;
- const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
+ const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue);
if (tab) {
tab.header.parent.setActiveContentItem(tab.contentItem);
return true;
@@ -193,7 +193,7 @@ export class CollectionDockingView extends CollectionSubView() {
const instance = CollectionDockingView.Instance;
const glayRoot = instance._goldenLayout.root;
if (!instance) return false;
- const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
+ const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
if (!pullSide && stack) {
stack.addChild(docContentConfig, undefined);
@@ -410,11 +410,10 @@ export class CollectionDockingView extends CollectionSubView() {
const _width = Number(getComputedStyle(content).width.replace('px', ''));
const _height = Number(getComputedStyle(content).height.replace('px', ''));
return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
- const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
proto['thumb-nativeWidth'] = _width;
proto['thumb-nativeHeight'] = _height;
- this.dataDoc.thumb = new ImageField(iconFile);
+ proto.thumb = new ImageField(iconFile);
});
}
}
@@ -602,6 +601,6 @@ ScriptingGlobals.add(
'opens up document in screen overlay layer',
'(doc: any)'
);
-ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) {
- CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey);
+ScriptingGlobals.add(function useRightSplit(doc: any, addToRightSplit?: boolean) {
+ CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, addToRightSplit);
});
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index c83f4e689..225743748 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -246,23 +246,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
immediate: undoBatch((source: Doc[]) => {}),
initialize: emptyFunction,
};
- _openLinkInCommand = {
- params: ['target', 'container'],
- title: 'link follow target',
- script: `{ if (self.container?.length) {
- getProto(self.target).linkContainer = self.container[0];
- getProto(self.target).isLinkButton = true;
- getProto(self.target).onClick = makeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])");
- }}`,
- immediate: undoBatch((container: Doc[]) => {
- if (container.length) {
- Doc.GetProto(this.target).linkContainer = container[0];
- Doc.GetProto(this.target).isLinkButton = true;
- Doc.GetProto(this.target).onClick = ScriptField.MakeScript('getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])');
- }
- }),
- initialize: emptyFunction,
- };
_viewCommand = {
params: ['target'],
title: 'bookmark view',
@@ -328,7 +311,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
return Doc.noviceMode ? undefined : [this._templateCommand, this._narrativeCommand];
}
@computed get _doc_commands() {
- return Doc.noviceMode ? undefined : [this._openLinkInCommand, this._onClickCommand];
+ return Doc.noviceMode ? undefined : [this._onClickCommand];
}
@computed get _tree_commands() {
return undefined;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index fbf7db892..aec0734b4 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -215,7 +215,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined);
// 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 dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
let dref: Opt<DocumentView>;
const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
@@ -293,7 +293,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
getDocHeight(d?: Doc) {
if (!d || d.hidden) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
- const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc;
+ const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc;
const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
@@ -441,7 +441,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
} 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
+ de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
e.stopPropagation();
} else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
return false;
@@ -515,6 +515,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
this.observer.observe(ref);
}
}}
+ select={this.props.select}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
columnHeaders={this.columnHeaders}
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 829d055e5..28bdd0cb9 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -37,6 +37,7 @@ interface CSVFieldColumnProps {
gridGap: number;
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
headings: () => object[];
+ select: (ctrlPressed: boolean) => void;
renderChildren: (docs: Doc[]) => JSX.Element[];
addDocument: (doc: Doc | Doc[]) => boolean;
createDropTarget: (ele: HTMLDivElement) => void;
@@ -240,7 +241,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
className="collectionNoteTakingView-sectionHeader-subCont"
title={evContents === `No Value` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}>
- <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
+ <EditableView GetValue={() => evContents} isEditingCallback={isEditing => isEditing && this.props.select(false)} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
</div>
{(this.props.columnHeaders?.length ?? 0) > 1 && (
<button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}>
@@ -267,7 +268,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
{this.props.renderChildren(this.props.docList)}
</div>
- {!this.props.chromeHidden && type !== DocumentType.PRES ? (
+ {!this.props.chromeHidden ? (
<div className="collectionNoteTakingView-DocumentButtons" style={{ 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} />
@@ -288,7 +289,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
const heading = this._heading;
return (
<div
- className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
+ className="collectionNoteTakingViewFieldColumn"
key={heading}
style={{
width: this.columnWidth,
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 5a107d2ca..a55b70e22 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -97,7 +97,7 @@
top: 2.5%;
height: 95%;
border-radius: 4px;
- background: $light-gray;
+ //background: $light-gray;
&:hover {
opacity: 1;
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 5bdff347c..bbd81d06d 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -7,14 +7,14 @@ import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
+import { Cast, NumCast, ScriptCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
-import { LinkFollower } from '../../util/LinkFollower';
+import { FollowLinkScript, IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
@@ -23,7 +23,6 @@ import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { AudioWaveform } from '../AudioWaveform';
import { CollectionSubView } from '../collections/CollectionSubView';
-import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { LabelBox } from '../nodes/LabelBox';
@@ -180,12 +179,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
if (
// need to include range inputs because after dragging video time slider it becomes target element
!(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
- this.props.isSelected(true)
+ this.props.isContentActive()
) {
// if shift pressed scrub 1 second otherwise 1/10th
const jump = e.shiftKey ? 1 : 0.1;
switch (e.key) {
case ' ':
+ this.props.playing() ? this.props.Pause() : this.props.Play();
+ break;
+ case '^':
if (!CollectionStackedTimeline.SelectingRegion) {
this._markerStart = this._markerEnd = this.currentTime;
CollectionStackedTimeline.SelectingRegion = this;
@@ -399,11 +401,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any,
_minFontSize: 12,
_maxFontSize: 24,
- _singleLine: false,
+ _singleLine: true,
_stayInCollection: true,
+ backgroundColor: 'rgba(128, 128, 128, 0.5)',
useLinkSmallAnchor: true,
hideLinkButton: true,
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
annotationOn: rootDoc,
_timelineLabel: true,
borderRounding: anchorEndTime === undefined ? '100%' : undefined,
@@ -422,7 +425,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@action
playOnClick = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
const endTime = this.anchorEnd(anchorDoc);
if (this.layoutDoc.autoPlayAnchors) {
if (this.props.playing()) this.props.Pause();
@@ -447,10 +450,10 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@action
clickAnchor = (anchorDoc: Doc, clientX: number) => {
- if (anchorDoc.isLinkButton) {
- LinkFollower.FollowLink(undefined, anchorDoc, this.props, false);
+ if (IsFollowLinkScript(anchorDoc.onClick)) {
+ LinkFollower.FollowLink(undefined, anchorDoc, false);
}
- const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
const endTime = this.anchorEnd(anchorDoc);
if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
if (this.props.playing()) this.props.Pause();
@@ -508,37 +511,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
currentTimecode = () => this.currentTime;
- @computed get renderDictation() {
- const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
- return !dictation ? null : (
- <div
- style={{
- position: 'absolute',
- height: '100%',
- top: this.timelineContentHeight,
- background: Colors.LIGHT_BLUE,
- }}>
- <DocumentView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- Document={dictation}
- PanelHeight={this.dictationHeight}
- isAnnotationOverlay={true}
- isDocumentActive={returnFalse}
- select={emptyFunction}
- NativeDimScaling={returnOne}
- xMargin={25}
- yMargin={10}
- ScreenToLocalTransform={this.dictationScreenToLocalTransform}
- whenChildContentsActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- renderDepth={this.props.renderDepth + 1}></DocumentView>
- </div>
- );
- }
-
// renders selection region on timeline
@computed get selectionContainer() {
const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
@@ -564,7 +536,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
anchor,
}));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- return (
+ return this.clipDuration === 0 ? null : (
<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
<div
className="collectionStackedTimeline-timelineContainer"
@@ -597,10 +569,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
top,
width: `${width}px`,
height: `${height}px`,
- }}
- onClick={e => {
- this.props.playFrom(start, this.anchorEnd(d.anchor));
- e.stopPropagation();
+ pointerEvents: 'none',
}}>
<StackedTimelineAnchor
{...this.props}
@@ -637,7 +606,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
PanelWidth={this.timelineContentWidth}
/>
)}
- {/* {this.renderDictation} */}
<div
className="collectionStackedTimeline-hover"
@@ -753,7 +721,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
time < NumCast(this.props.mark[this.props.endTag]) &&
this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5
) {
- LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false);
+ LinkFollower.FollowLink(undefined, this.props.mark, false);
}
this._lastTimecode = time;
}
@@ -764,7 +732,9 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this._disposer?.();
}
+ @observable noEvents = false;
// starting the drag event for anchor resizing
+ @action
onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
//this.props._timeline?.setPointerCapture(e.pointerId);
const newTime = (e: PointerEvent) => {
@@ -782,8 +752,8 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
}
return false;
};
+ this.noEvents = true;
var undo: UndoManager.Batch | undefined;
-
setupMoveUpEvents(
this,
e,
@@ -792,11 +762,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this.props.setTime(newTime(e));
return changeAnchor(anchor, left, newTime(e));
},
- e => {
+ action(e => {
this.props.setTime(newTime(e));
- // this.props._timeline?.releasePointerCapture(e.pointerId);
undo?.end();
- },
+ this.noEvents = false;
+ }),
emptyFunction
);
};
@@ -814,19 +784,23 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
// renders anchor LabelBox
renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
const anchor = observable({ view: undefined as any });
- const focusFunc = (doc: Doc, options: DocFocusOptions) => {
+ const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => {
this.props.playLink(mark);
- this.props.focus(doc, options);
+ return undefined;
};
return {
anchor,
view: (
<DocumentView
key="view"
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
ref={action((r: DocumentView | null) => (anchor.view = r))}
Document={mark}
DataDoc={undefined}
+ docViewPath={returnEmptyDoclist}
+ pointerEvents={this.noEvents ? returnNone : undefined}
styleProvider={this.props.styleProvider}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={undefined}
@@ -836,7 +810,16 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
PanelHeight={height}
fitWidth={returnTrue}
ScreenToLocalTransform={screenXf}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
focus={focusFunc}
+ isContentActive={returnFalse}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ searchFilterDocs={returnEmptyDoclist}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
rootSelected={returnFalse}
onClick={script}
onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
@@ -857,15 +840,15 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
render() {
const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.anchorScreenToLocalXf, this.width, this.height);
return (
- <>
+ <div style={{ pointerEvents: this.noEvents ? 'none' : undefined }}>
{inner.view}
{!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? null : (
<>
- <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
- <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
+ <div key="left" className="collectionStackedTimeline-left-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
+ <div key="right" className="collectionStackedTimeline-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
</>
)}
- </>
+ </div>
);
}
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 4805a748b..a85ee0e02 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -3,14 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CursorProperty } from 'csstype';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
@@ -201,6 +201,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
componentDidMount() {
super.componentDidMount?.();
+ this.props.setContentView?.(this);
// reset section headers when a new filter is inputted
this._pivotFieldDisposer = reaction(
@@ -226,6 +227,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this._autoHeightDisposer?.();
}
+ isAnyChildContentActive = () => this.props.isAnyChildContentActive();
+
@action
moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
@@ -302,7 +305,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// this is what renders the document that you see on the screen
// called in Children: this actually adds a document to our children list
getDisplayDoc(doc: Doc, width: () => number, count: number) {
- const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
+ const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
let dref: Opt<DocumentView>;
@@ -320,8 +323,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
styleProvider={this.styleProvider}
docViewPath={this.props.docViewPath}
fitWidth={this.props.childFitWidth}
- isContentActive={doc.isLinkButton ? this.isChildButtonContentActive : this.isChildContentActive}
+ isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive}
onKey={this.onKeyDown}
+ onBrowseClick={this.props.onBrowseClick}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
@@ -377,7 +381,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
getDocHeight(d?: Doc) {
if (!d || d.hidden) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
- const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc;
+ const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc;
const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
@@ -456,7 +460,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
} 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
+ de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
e.stopPropagation();
} else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
return false;
@@ -619,11 +623,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!e.isPropagationStopped()) {
- const subItems: ContextMenuProps[] = [];
- subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
- subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
- subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+ optionItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+ optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
}
};
@@ -651,11 +657,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
- isContentActive={this.props.isContentActive}
- isDocumentActive={returnTrue}
+ isContentActive={this.isContentActive}
+ isDocumentActive={this.isContentActive}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
addDocTab={this.props.addDocTab}
+ onBrowseClick={this.props.onBrowseClick}
pinToPres={emptyFunction}
rootSelected={this.props.isSelected}
removeDocument={this.props.removeDocument}
@@ -716,28 +723,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
ref={this.createRef}
style={{
- overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
+ overflowY: this.isContentActive() ? 'auto' : 'hidden',
background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
- pointerEvents: this.backgroundEvents ? 'all' : undefined,
+ pointerEvents: (this.props.pointerEvents?.() as any) ?? (this.backgroundEvents ? 'all' : undefined),
}}
onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
onDrop={this.onExternalDrop.bind(this)}
onContextMenu={this.onContextMenu}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}>
+ onWheel={e => this.isContentActive() && e.stopPropagation()}>
{this.renderedSections}
{!this.showAddAGroup ? null : (
<div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
<EditableView {...editableViewProps} />
</div>
)}
- {/* {this.chromeHidden || !this.props.isSelected() ? (null) :
- <Switch
- onChange={this.onToggle}
- onClick={this.onToggle}
- defaultChecked={true}
- checkedChildren="edit"
- unCheckedChildren="view"
- />} */}
</div>
</div>
</>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e46220f02..132ed6fb6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -79,7 +79,7 @@ export function CollectionSubView<X>(moreProps?: X) {
.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc))
.filter(pair => {
// filter out any documents that have a proto that we don't have permissions to
- return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
+ return !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
});
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
@@ -269,10 +269,10 @@ export function CollectionSubView<X>(moreProps?: X) {
if (FormattedTextBox.IsFragment(html)) {
const href = FormattedTextBox.GetHref(html);
if (href) {
- const docid = FormattedTextBox.GetDocFromUrl(href);
- if (docid) {
+ const docId = FormattedTextBox.GetDocFromUrl(href);
+ if (docId) {
// prosemirror text containing link to dash document
- DocServer.GetRefField(docid).then(f => {
+ DocServer.GetRefField(docId).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) {
f.x = options.x as number;
@@ -311,8 +311,8 @@ export function CollectionSubView<X>(moreProps?: X) {
} else {
const path = window.location.origin + '/doc/';
if (text.startsWith(path)) {
- const docid = text.replace(Doc.globalServerPath(), '').split('?')[0];
- DocServer.GetRefField(docid).then(f => {
+ const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
+ DocServer.GetRefField(docId).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) {
f.x = options.x as number;
@@ -335,7 +335,7 @@ export function CollectionSubView<X>(moreProps?: X) {
const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
if (focusNode) {
const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
- anchor && DocUtils.MakeLink({ doc: htmlDoc }, { doc: anchor });
+ anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
}
}
}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 89b2fbfe3..4d5978548 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -150,7 +150,7 @@ export class CollectionTimeView extends CollectionSubView() {
engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
fitContentsToBox={returnTrue}
childClickScript={this._childClickedScript}
- viewDefDivClick={this._viewDefDivClick}
+ viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
//dontScaleFilter={this.dontScaleFilter}
layoutEngine={this.layoutEngine}
/>
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 553967b95..4a11e8f0b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -7,7 +7,7 @@ import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../Utils';
+import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
@@ -384,12 +384,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false;
@observable _headerHeight = 0;
- contentFunc = () => {
+ @computed get content() {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
- return [
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
{!this.buttonMenu && !this.noviceExplainer ? null : (
<div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}>
{this.buttonMenu}
@@ -428,9 +428,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
</div>
</div>
</div>
- </div>,
- ];
- };
+ </div>
+ );
+ }
render() {
TraceMobx();
@@ -439,7 +439,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
{!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? (
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
isAnnotationOverlay={true}
isAnnotationOverlayScrollable={true}
childDocumentsActive={this.props.isDocumentActive}
@@ -451,10 +455,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
moveDocument={this.moveAnnotationDocument}
bringToFront={emptyFunction}
renderDepth={this.props.renderDepth + 1}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
) : (
- this.contentFunc()
+ this.content
)}
</div>
);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 24f77ec0e..d7d981109 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
import { ObjectField } from '../../../fields/ObjectField';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -10,13 +9,12 @@ import { TraceMobx } from '../../../fields/util';
import { returnEmptyString } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
-import { BranchCreate, BranchTask } from '../../documents/Gitlike';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OpenWhere } from '../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { CollectionCarousel3DView } from './CollectionCarousel3DView';
import { CollectionCarouselView } from './CollectionCarouselView';
@@ -47,7 +45,7 @@ interface CollectionViewProps_ extends FieldViewProps {
// property overrides for child documents
childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
- childDocumentsActive?: () => boolean; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+ childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
childFitWidth?: (child: Doc) => boolean;
childShowTitle?: () => string;
childOpacity?: () => number;
@@ -195,23 +193,23 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
!Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
- if (!Doc.noviceMode && false) {
- optionItems.push({
- description: 'Create Branch',
- event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
- icon: 'project-diagram',
- });
- optionItems.push({
- description: 'Pull Master',
- event: () => BranchTask(this.rootDoc, 'pull'),
- icon: 'project-diagram',
- });
- optionItems.push({
- description: 'Merge Branches',
- event: () => BranchTask(this.rootDoc, 'merge'),
- icon: 'project-diagram',
- });
- }
+ // if (!Doc.noviceMode && false) {
+ // optionItems.push({
+ // description: 'Create Branch',
+ // event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
+ // icon: 'project-diagram',
+ // });
+ // optionItems.push({
+ // description: 'Pull Master',
+ // event: () => BranchTask(this.rootDoc, 'pull'),
+ // icon: 'project-diagram',
+ // });
+ // optionItems.push({
+ // description: 'Merge Branches',
+ // event: () => BranchTask(this.rootDoc, 'merge'),
+ // icon: 'project-diagram',
+ // });
+ // }
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' });
@@ -261,7 +259,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
return StrCast(this.rootDoc.childLayoutString, this.props.childLayoutString);
}
- isContentActive = (outsideReaction?: boolean) => this.props.isContentActive();
+ isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() || this.isAnyChildContentActive();
render() {
TraceMobx();
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 9459aaf1e..458712999 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -10,7 +10,6 @@ import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { FieldId } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
import { DocServer } from '../../DocServer';
@@ -28,6 +27,7 @@ import { LightboxView } from '../LightboxView';
import { MainView } from '../MainView';
import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
import { DashFieldView } from '../nodes/formattedText/DashFieldView';
+import { KeyValueBox } from '../nodes/KeyValueBox';
import { PinProps, PresBox, PresMovement } from '../nodes/trails';
import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
import { CollectionDockingView } from './CollectionDockingView';
@@ -39,6 +39,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
interface TabDocViewProps {
documentId: FieldId;
+ keyValue?: boolean;
glContainer: any;
}
@observer
@@ -57,7 +58,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
@computed get tabBorderColor() {
const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting);
- if (highlight?.highlightIndex >= Doc.DocBrushStatus.highlighted) return highlight.highlightColor;
+ if (highlight?.highlightIndex === Doc.DocBrushStatus.highlighted) return highlight.highlightColor;
return 'transparent';
}
@computed get tabColor() {
@@ -348,7 +349,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
addDocTab = (doc: Doc, location: OpenWhere) => {
SelectionManager.DeselectAll();
const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
- const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
+ const keyValue = whereFields[1]?.includes('KeyValue');
+ const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
if (doc.dockingConfig) return DashboardView.openDashboard(doc);
// prettier-ignore
switch (whereFields[0]) {
@@ -365,9 +367,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
case OpenWhere.dashboard: return DashboardView.openDashboard(doc);
case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods);
- case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack);
- case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack);
- case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack);
+ case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, undefined, keyValue);
+ case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, undefined, keyValue);
+ case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue);
}
};
remDocTab = (doc: Doc | Doc[]) => {
@@ -419,12 +421,16 @@ export class TabDocView extends React.Component<TabDocViewProps> {
this._lastView = this._view;
})}
renderDepth={0}
+ LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString() : undefined}
+ hideTitle={this.props.keyValue}
Document={this._document}
DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
onBrowseClick={MainView.Instance.exploreMode}
+ waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick}
isContentActive={returnTrue}
+ isDocumentActive={returnFalse}
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index af2d148e0..75e76019e 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -305,7 +305,7 @@ export class TreeView extends React.Component<TreeViewProps> {
};
public static makeTextBullet() {
- const bullet = Docs.Create.TextDocument('-text-', {
+ const bullet = Docs.Create.TextDocument('', {
layout: CollectionView.LayoutString('data'),
title: '-title-',
treeViewExpandedViewLock: true,
@@ -326,7 +326,8 @@ export class TreeView extends React.Component<TreeViewProps> {
});
Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
Doc.GetProto(bullet).data = new List<Doc>([]);
- FormattedTextBox.SelectOnLoad = bullet[Id];
+ DocumentManager.Instance.AddViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());
+
return bullet;
}
@@ -358,7 +359,7 @@ export class TreeView extends React.Component<TreeViewProps> {
if (de.complete.linkDragData) {
const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
const destDoc = this.doc;
- DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, 'tree link', '');
+ DocUtils.MakeLink(sourceDoc, destDoc, { linkRelationship: 'tree link' });
e.stopPropagation();
}
const docDragData = de.complete.docDragData;
@@ -572,6 +573,7 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>
)}
<ul
+ style={{ cursor: 'inherit' }}
key={expandKey + 'more'}
title="click to change sort order"
className={''} //this.doc.treeViewHideTitle ? 'no-indent' : ''}
@@ -628,7 +630,7 @@ export class TreeView extends React.Component<TreeViewProps> {
);
} else if (this.treeViewExpandedView === 'fields') {
return (
- <ul key={this.doc[Id] + this.doc.title}>
+ <ul key={this.doc[Id] + this.doc.title} style={{ cursor: 'inherit' }}>
<div>{this.expandedField}</div>
</ul>
);
@@ -902,6 +904,8 @@ export class TreeView extends React.Component<TreeViewProps> {
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
styleProvider={this.titleStyleProvider}
+ enableDragWhenActive={true}
+ onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
docViewPath={returnEmptyDoclist}
treeViewDoc={this.props.treeView.props.Document}
addDocument={undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index fa0695fb2..81b0c4d8a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -116,6 +116,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
zIndex: NumCast(layout.zIndex),
pair: { layout, data },
replica: '',
+ color: 'white',
+ backgroundColor: 'white',
});
});
const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
@@ -408,7 +410,7 @@ function normalizeResults(
.map(ele => {
const newPosRaw = ele[1];
if (newPosRaw) {
- const newPos = {
+ const newPos: PoolData = {
x: newPosRaw.x * scale,
y: newPosRaw.y * scale,
z: newPosRaw.z,
@@ -417,6 +419,9 @@ function normalizeResults(
zIndex: newPosRaw.zIndex,
width: (newPosRaw.width || 0) * scale,
height: newPosRaw.height! * scale,
+ backgroundColor: newPosRaw.backgroundColor,
+ opacity: newPosRaw.opacity,
+ color: newPosRaw.color,
pair: ele[1].pair,
};
poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f0c140ef1..7ae7be3c8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
import { Bezier } from 'bezier-js';
import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import { DateField } from '../../../../fields/DateField';
@@ -53,10 +53,14 @@ import { MarqueeView } from './MarqueeView';
import React = require('react');
export type collectionFreeformViewProps = {
+ noPointerWheel?: () => boolean; // turn off pointerwheel interactions (see PDFViewer)
+ NativeWidth?: () => number;
+ NativeHeight?: () => number;
+ originTopLeft?: boolean;
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
viewDefDivClick?: ScriptField;
childPointerEvents?: string;
- scaleField?: string;
+ viewField?: string;
noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
engineProps?: any;
getScrollHeight?: () => number | undefined;
@@ -97,8 +101,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
private get isAnnotationOverlay() {
return this.props.isAnnotationOverlay;
}
- private get scaleFieldKey() {
- return this.props.scaleField || '_viewScale';
+ public get scaleFieldKey() {
+ return this.props.viewField ? this.props.viewField + '-viewScale' : '_viewScale';
+ }
+ private get panXFieldKey() {
+ return this.props.viewField ? this.props.viewField + '-panX' : '_panX';
+ }
+ private get panYFieldKey() {
+ return this.props.viewField ? this.props.viewField + '-panY' : '_panY';
}
private get borderWidth() {
return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH;
@@ -120,7 +130,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
constructor(props: any) {
super(props);
- this.props.setContentView?.(this);
}
@computed get views() {
@@ -150,21 +159,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
);
}
@computed get nativeWidth() {
- return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+ return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
}
@computed get nativeHeight() {
- return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+ return this.props.NativeHeight?.() || (this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
}
@computed get cachedCenteringShiftX(): number {
const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
- return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
const dv = this.props.DocumentView?.();
const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
// if freeform has a native aspect, then the panel height needs to be adjusted to match it
const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
- return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
return Transform.Identity()
@@ -237,13 +246,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
reverseNativeScaling = () => (this.fitContentsToBox ? true : false);
// panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
// this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
- panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
- panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
+ panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
+ panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
contentTransform = () =>
- !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1
- ? ''
- : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ this.props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
getContainerTransform = () => this.cachedGetContainerTransform.copy();
@@ -293,14 +300,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
}
+ groupFocus = (anchor: Doc, options: DocFocusOptions) => {
+ options.docTransform = new Transform(-NumCast(this.rootDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.rootDoc[this.panYFieldKey]) + NumCast(anchor.y), 1);
+ const res = this.props.focus(this.rootDoc, options);
+ options.docTransform = undefined;
+ return res;
+ };
+
focus = (anchor: Doc, options: DocFocusOptions) => {
const xfToCollection = options?.docTransform ?? Transform.Identity();
- const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
+ const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale || 0.75 : undefined);
// focus on the document in the collection
const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
+ if (didMove) options.didMove = true;
// glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
if (didMove) {
const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
@@ -312,6 +327,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
getView = async (doc: Doc): Promise<Opt<DocumentView>> => {
return new Promise<Opt<DocumentView>>(res => {
+ doc.hidden && (doc.hidden = false);
const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
findDoc(dv => res(dv));
});
@@ -333,6 +349,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
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 ? [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);
@@ -354,6 +371,30 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
(d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront
}
+ if (this.layoutDoc._autoArrange || de.metaKey) {
+ const sorted = this.childLayoutPairs.slice().sort((a, b) => NumCast(a.layout.y) - NumCast(b.layout.y));
+ sorted.splice(
+ sorted.findIndex(pair => pair.layout === refDoc),
+ 1
+ );
+ if (sorted.length && refDoc && NumCast(sorted[0].layout.y) < NumCast(refDoc.y)) {
+ const topIndexed = NumCast(refDoc.y) < NumCast(sorted[0].layout.y) + NumCast(sorted[0].layout._height);
+ const deltay = sorted.length > 1 ? NumCast(refDoc.y) - (NumCast(sorted[0].layout.y) + (topIndexed ? 0 : NumCast(sorted[0].layout._height))) : 0;
+ const deltax = sorted.length > 1 ? NumCast(refDoc.x) - NumCast(sorted[0].layout.x) : 0;
+
+ let lastx = NumCast(refDoc.x);
+ let lasty = NumCast(refDoc.y) + (topIndexed ? 0 : NumCast(refDoc._height));
+ setTimeout(
+ action(() =>
+ sorted.slice(1).forEach((pair, i) => {
+ lastx = pair.layout.x = lastx + deltax;
+ lasty = (pair.layout.y = lasty + deltay) + (topIndexed ? 0 : NumCast(pair.layout._height));
+ })
+ )
+ );
+ }
+ }
+
(docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments);
return true;
}
@@ -381,7 +422,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' });
this.props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, 'annotated by:annotation of', ''); // TODODO this is where in text links get passed
+ de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { linkRelationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
}
e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document
return true;
@@ -648,8 +689,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
ActiveIsInkMask(),
{
title: 'ink stroke',
- x: B.x - ActiveInkWidth() / 2,
- y: B.y - ActiveInkWidth() / 2,
+ x: B.x - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2,
+ y: B.y - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2,
_width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
_height: B.height + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
}
@@ -764,7 +805,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
PresBox.Instance?.pauseAutoPres();
const dx = e.deltaX;
const dy = e.deltaY;
- this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+ this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
};
@action
@@ -772,7 +813,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
PresBox.Instance?.pauseAutoPres();
this.props.DocumentView?.().clearViewTransition();
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+ this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
this._lastX = e.clientX;
this._lastY = e.clientY;
};
@@ -813,8 +854,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false;
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
Doc.ActiveTool = InkTool.None;
- if (this.props.isContentActive(true)) e.stopPropagation();
- } else if (!e.cancelBubble) {
+ } else {
if (this.tryDragCluster(e, this._hitCluster)) {
return true;
}
@@ -946,96 +986,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return tVals;
};
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- if (myTouches[0]) {
- if (Doc.ActiveTool === InkTool.None) {
- if (this.tryDragCluster(e, this._hitCluster)) {
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- document.removeEventListener('pointermove', this.onPointerMove);
- return;
- }
- // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected'
- this.pan(myTouches[0]);
- }
- }
- // e.stopPropagation();
- e.preventDefault();
- }
- };
-
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- // pinch zooming
- if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
-
- if (this.prevPoints.size === 2) {
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- if (oldPoint1 && oldPoint2) {
- const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
-
- // if zooming, zoom
- if (dir !== 0) {
- const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
- const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-
- // calculate the raw delta value
- const rawDelta = dir * (d1 + d2);
-
- // this floors and ceils the delta value to prevent jitteriness
- const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8);
- this.zoom(centerX, centerY, delta * window.devicePixelRatio);
- this.prevPoints.set(pt1.identifier, pt1);
- this.prevPoints.set(pt2.identifier, pt2);
- }
- // this is not zooming. derive some form of panning from it.
- else {
- // use the centerx and centery as the "new mouse position"
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
- // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
-
- this._lastX = centerX;
- this._lastY = centerY;
- }
- }
- }
- // e.stopPropagation();
- e.preventDefault();
- }
- };
-
- @action
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (this.props.isContentActive(true)) {
- // const pt1: React.Touch | null = e.targetTouches.item(0);
- // const pt2: React.Touch | null = e.targetTouches.item(1);
- // // if (!pt1 || !pt2) return;
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- if (pt1 && pt2) {
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
- this._lastX = centerX;
- this._lastY = centerY;
-
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
- }
- }
- };
-
cleanUpInteractions = () => {
this.removeMoveListeners();
this.removeEndListeners();
@@ -1062,13 +1012,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
- this.setPan(-localTransform.TranslateX / safeScale, NumCast(this.props.Document.scrollTop) * safeScale || -localTransform.TranslateY / safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.scrollTop) * safeScale) || -localTransform.TranslateY / safeScale);
}
};
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.Document._isGroup) return; // group style collections neither pan nor zoom
+ if (this.props.noPointerWheel?.() || this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
PresBox.Instance?.pauseAutoPres();
if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
e.stopPropagation();
@@ -1078,7 +1028,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// if ctrl is selected then zoom
if (e.ctrlKey) {
if (this.props.isContentActive(true)) {
- !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
}
} // otherwise pan
else if (this.props.isContentActive(true)) {
@@ -1122,16 +1072,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) },
}),
{
- xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
- yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
+ xrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
+ yrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
}
);
- const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()];
- if (ranges.xrange.min >= panX + panelDim[0] / 2) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds
- else if (ranges.xrange.max <= panX - panelDim[0] / 2) panX = ranges.xrange.min - panelDim[0] / 2;
- if (ranges.yrange.min >= panY + panelDim[1] / 2) panY = ranges.yrange.max + panelDim[1] / 2;
- else if (ranges.yrange.max <= panY - panelDim[1] / 2) panY = ranges.yrange.min - panelDim[1] / 2;
+ const panelWidMax = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+ const panelWidMin = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+ const panelHgtMax = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+ const panelHgtMin = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+ if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2);
+ else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2);
+ if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2);
+ else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
}
}
if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
@@ -1161,8 +1114,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}, 10);
newPanY = minPanY;
}
- !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
- !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
+ !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX);
+ !this.Document._horizontalScroll && (this.Document[this.panYFieldKey] = this.isAnnotationOverlay ? newPanY : panY);
}
}
@@ -1170,8 +1123,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
nudge = (x: number, y: number, nudgeTime: number = 500) => {
if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) {
this.setPan(
- NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
- NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
+ NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
+ NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
nudgeTime,
true
);
@@ -1189,12 +1142,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
} else {
const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
- let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
- if (zlast - docs.length > 100) {
- for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
- zlast = docs.length + 1;
+ let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1;
+ if (docs.lastElement() !== doc) {
+ if (zlast - docs.length > 100) {
+ for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
+ zlast = docs.length + 1;
+ }
+ doc.zIndex = zlast + 1;
}
- doc.zIndex = zlast + 1;
}
};
@@ -1217,8 +1172,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
- this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
- this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
+ this.layoutDoc[this.panXFieldKey] = NumCast(this.layoutDoc[this.panXFieldKey]) - newpan[0];
+ this.layoutDoc[this.panYFieldKey] = NumCast(this.layoutDoc[this.panYFieldKey]) - newpan[1];
}
calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
@@ -1241,8 +1196,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight();
const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1);
const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1);
- const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
- const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
+ const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
+ const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
@@ -1254,8 +1209,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
}
return {
- panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
- panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
+ panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+ panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
};
};
@@ -1284,10 +1239,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
pointerEvents = () => {
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
- const pointerEvents =
- this.props.isContentActive() === false || DocumentDecorations.Instance.Interacting
- ? 'none'
- : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+ const pointerEvents = DocumentDecorations.Instance.Interacting
+ ? 'none'
+ : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
return pointerEvents;
};
getChildDocView(entry: PoolData) {
@@ -1308,6 +1262,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
LayoutTemplate={childLayout.z ? undefined : this.props.childLayoutTemplate}
LayoutTemplateString={childLayout.z ? undefined : this.props.childLayoutString}
rootSelected={childData ? this.rootSelected : returnFalse}
+ waitForDoubleClickToClick={this.props.waitForDoubleClickToClick}
onClick={this.onChildClickHandler}
onKey={this.onKeyDown}
onDoubleClick={this.onChildDoubleClickHandler}
@@ -1320,7 +1275,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
searchFilterDocs={this.searchFilterDocs}
isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
isContentActive={emptyFunction}
- focus={this.isAnnotationOverlay ? this.props.focus : this.focus}
+ focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
@@ -1351,7 +1306,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
case OpenWhere.inParentFromScreen:
const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.context);
return (
- (this.props.addDocument?.(
+ (this.addDocument?.(
(doc instanceof Doc ? [doc] : doc).map(doc => {
const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
doc.x = pt[0];
@@ -1543,7 +1498,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) {
// don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
- if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
+ if (this.props.viewField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey]));
}
@@ -1568,6 +1523,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
componentDidMount() {
+ this.props.setContentView?.(this);
super.componentDidMount?.();
this.props.setBrushViewer?.(this.brushView);
setTimeout(
@@ -1584,7 +1540,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
cbounds => {
if (cbounds) {
const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2];
- const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)];
+ const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])];
const pbounds = {
x: cbounds.x - p[0] + c[0],
y: cbounds.y - p[1] + c[1],
@@ -1594,8 +1550,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (Number.isFinite(pbounds.r - pbounds.x) && Number.isFinite(pbounds.b - pbounds.y)) {
this.layoutDoc._width = pbounds.r - pbounds.x;
this.layoutDoc._height = pbounds.b - pbounds.y;
- this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2;
- this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2;
+ this.layoutDoc[this.panXFieldKey] = (cbounds.r + cbounds.x) / 2;
+ this.layoutDoc[this.panYFieldKey] = (cbounds.b + cbounds.y) / 2;
this.layoutDoc.x = pbounds.x;
this.layoutDoc.y = pbounds.y;
}
@@ -1628,7 +1584,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
} else {
const canvas = oldDiv;
const img = document.createElement('img'); // create a Image Element
- img.src = canvas.toDataURL(); //image source
+ try {
+ img.src = canvas.toDataURL(); //image source
+ } catch (e) {
+ console.log(e);
+ }
img.style.width = canvas.style.width;
img.style.height = canvas.style.height;
const newCan = newDiv as HTMLCanvasElement;
@@ -1713,8 +1673,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
if (deltaX !== 0 || deltaY !== 0) {
- this.Document._panY = NumCast(this.Document._panY) + deltaY / 2;
- this.Document._panX = NumCast(this.Document._panX) + deltaX / 2;
+ this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2;
+ this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2;
}
}
e.stopPropagation();
@@ -1738,8 +1698,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20;
const dim = Math.ceil(Math.sqrt(docs.length));
docs.forEach((doc, i) => {
- doc.x = NumCast(this.Document._panX) + (i % dim) * width - (width * dim) / 2;
- doc.y = NumCast(this.Document._panY) + Math.floor(i / dim) * height - (height * dim) / 2;
+ doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2;
+ doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2;
});
};
@@ -1754,11 +1714,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
appearanceItems.push({
description: 'Reset View',
event: () => {
- this.props.Document._panX = this.props.Document._panY = 0;
+ this.props.Document[this.panXFieldKey] = this.props.Document[this.panYFieldKey] = 0;
this.props.Document[this.scaleFieldKey] = 1;
},
icon: 'compress-arrows-alt',
});
+ !Doc.noviceMode &&
+ appearanceItems.push({
+ description: 'Toggle auto arrange',
+ event: () => (this.layoutDoc._autoArrange = !this.layoutDoc._autoArrange),
+ icon: 'compress-arrows-alt',
+ });
+ if (this.props.setContentView === emptyFunction) {
+ !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
+ return;
+ }
!Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
appearanceItems.push({
description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`,
@@ -1766,8 +1736,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt',
});
appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' });
- //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
- appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
+ !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
+ this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' });
@@ -1796,27 +1766,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const mores = ContextMenu.Instance.findByDescription('More...');
const moreItems = mores && 'subitems' in mores ? mores.subitems : [];
- moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
!mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
};
- importDocument = (x: number, y: number) => {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.zip';
- input.onchange = _e => {
- input.files &&
- Doc.importDocument(input.files[0]).then(doc => {
- if (doc instanceof Doc) {
- const [xx, yy] = this.getTransform().transformPoint(x, y);
- (doc.x = xx), (doc.y = yy);
- this.props.addDocument?.(doc);
- }
- });
- };
- input.click();
- };
-
@undoBatch
@action
transcribeStrokes = (math: boolean) => {
@@ -1874,11 +1826,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
});
- children = () => {
+ get children() {
this.incrementalRender();
- const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : [];
+ const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : [];
return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />];
- };
+ }
@computed get placeholder() {
return (
@@ -1922,6 +1874,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
PanelHeight={this.props.PanelHeight}
panX={this.panX}
panY={this.panY}
+ nativeDimScaling={this.nativeDim}
zoomScaling={this.zoomScaling}
layoutDoc={this.layoutDoc}
isAnnotationOverlay={this.isAnnotationOverlay}
@@ -1956,6 +1909,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale;
}
+ nativeDim = () => this.nativeDimScaling;
private groupDropDisposer?: DragManager.DragDropDisposer;
protected createGroupEventsTarget = (ele: HTMLDivElement) => {
@@ -1987,8 +1941,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
TraceMobx();
return (
<div
- className={'collectionfreeformview-container'}
- ref={this.createDashEventsTarget}
+ className="collectionfreeformview-container"
+ ref={r => {
+ this.createDashEventsTarget(r);
+ // prevent wheel events from passivly propagating up through containers
+ !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
+ }}
onWheel={this.onPointerWheel}
onClick={this.onClick}
onPointerDown={this.onPointerDown}
@@ -2060,7 +2018,8 @@ interface CollectionFreeFormViewPannableContentsProps {
transform: () => string;
zoomScaling: () => number;
viewDefDivClick?: ScriptField;
- children: () => JSX.Element[];
+ children?: React.ReactNode | undefined;
+ //children: () => JSX.Element[];
transition?: string;
presPaths: () => JSX.Element | null;
presPinView?: boolean;
@@ -2154,7 +2113,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
//willChange: "transform"
}}>
- {this.props.children()}
+ {this.props.children}
{!this.props.brushView.width ? null : (
<div
className="collectionFreeFormView-brushView"
@@ -2182,6 +2141,7 @@ interface CollectionFreeFormViewBackgroundGridProps {
PanelWidth: () => number;
PanelHeight: () => number;
isAnnotationOverlay?: boolean;
+ nativeDimScaling: () => number;
zoomScaling: () => number;
layoutDoc: Doc;
cachedCenteringShiftX: number;
@@ -2199,10 +2159,10 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling();
const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling();
const renderGridSpace = gridSpace * this.props.zoomScaling();
- const w = this.props.PanelWidth() + 2 * renderGridSpace;
- const h = this.props.PanelHeight() + 2 * renderGridSpace;
+ const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace;
+ const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace;
const strokeStyle = Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'rgba(255,255,255,0.5)' : 'rgba(0, 0,0,0.5)';
- return (
+ return !this.props.nativeDimScaling() ? null : (
<canvas
className="collectionFreeFormView-grid"
width={w}
@@ -2246,11 +2206,14 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
zoomTime: browseTransitionTime,
}) === undefined
) {
- const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
- const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview
+ const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
+ let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview
ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
Doc.linkFollowHighlight(dv?.props.Document, false);
+ } else {
+ DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true });
}
}
ScriptingGlobals.add(CollectionBrowseClick);
@@ -2265,9 +2228,27 @@ ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent';
runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
});
-ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
- !readOnly &&
- SelectionManager.Views().forEach(view =>
- TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })
- );
+ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
+ SelectionManager.Views().forEach(view =>
+ view.props.pinToPres(view.rootDoc, {
+ currentFrame: Cast(view.rootDoc.currentFrame, 'number', null),
+ pinData: {
+ poslayoutview: pinContent,
+ dataview: pinContent,
+ },
+ pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()),
+ })
+ );
+});
+ScriptingGlobals.add(function bringToFront() {
+ SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc));
+});
+ScriptingGlobals.add(function sendToBack(doc: Doc) {
+ SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true));
+});
+ScriptingGlobals.add(function resetView() {
+ SelectionManager.Docs().forEach(doc => {
+ doc._panX = doc._panY = 0;
+ doc._viewScale = 1;
+ });
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 9581563ce..c9168d40a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,8 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { observer } from 'mobx-react';
import { unimplementedFunction } from '../../../../Utils';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { SelectionManager } from '../../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
@observer
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 0714bffbc..d443df0f3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -237,6 +237,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onPointerDown = (e: React.PointerEvent): void => {
+ // if (this.props.pointerEvents?.() === 'none') return;
this._downX = this._lastX = e.clientX;
this._downY = this._lastY = e.clientY;
if (!(e.nativeEvent as any).marqueeHit) {
@@ -345,6 +346,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onClick = (e: React.MouseEvent): void => {
+ if (this.props.pointerEvents?.() === 'none') return;
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
if (Doc.ActiveTool === InkTool.None) {
if (!(e.nativeEvent as any).marqueeHit) {
@@ -394,6 +396,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
newCollection._height = this.Bounds.height;
newCollection._isGroup = makeGroup;
newCollection.forceActive = makeGroup;
+ newCollection.enableDragWhenActive = makeGroup;
newCollection.x = this.Bounds.left;
newCollection.y = this.Bounds.top;
newCollection.fitWidth = true;
@@ -525,7 +528,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
});
const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
- DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
+ DocUtils.MakeLink(summary, portal, { linkRelationship: 'summary of:summarized by' });
portal.hidden = true;
this.props.addDocument?.(portal);
@@ -609,7 +612,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
return false;
}
- marqueeSelect(selectBackgrounds: boolean = true) {
+ marqueeSelect(selectBackgrounds: boolean = false) {
const selection: Doc[] = [];
const selectFunc = (doc: Doc) => {
const layoutDoc = Doc.Layout(doc);
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 9468c5f06..e8ae88ae5 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { Doc, Opt } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -186,7 +186,10 @@ export class CollectionGridView extends CollectionSubView() {
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return (
<DocumentView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ setContentView={emptyFunction}
Document={layout}
DataDoc={layout.resolvedDataDoc as Doc}
isContentActive={this.isChildContentActive}
@@ -196,7 +199,7 @@ export class CollectionGridView extends CollectionSubView() {
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
onClick={this.onChildClickHandler}
renderDepth={this.props.renderDepth + 1}
- dontCenter={this.props.Document.centerY ? '' : 'y'}
+ dontCenter={this.props.Document.centerY ? undefined : 'y'}
/>
);
}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 521bcda1e..3e3709827 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -1,9 +1,13 @@
@import '../../global/globalCssVariables';
@import '../../_nodeModuleOverrides';
+.collectionLinearView {
+ width: 100%;
+}
.collectionLinearView-label {
color: black;
background-color: $light-gray;
+ width: 100%;
}
.collectionLinearView-outer {
overflow: visible;
@@ -15,8 +19,6 @@
}
&.true {
- padding-left: 5px;
- padding-right: 5px;
border-left: $standard-border;
background-color: $medium-blue-alt;
}
@@ -29,7 +31,6 @@
display: flex;
height: 100%;
align-items: center;
- gap: 5px;
.collectionView {
overflow: visible !important;
@@ -101,13 +102,12 @@
background-color: $medium-blue;
padding: 5;
border-radius: 2px;
- height: 25;
+ height: 100%;
min-width: 25;
margin: 0;
color: $white;
display: flex;
font-weight: 100;
- width: fit-content;
transition: transform 0.2s;
align-items: center;
justify-content: center;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index d54e8ce98..c7d9b6619 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,5 +1,5 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { StylesProvider, Tooltip } from '@material-ui/core';
+import { Tooltip } from '@material-ui/core';
import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -11,8 +11,6 @@ import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
-import { Colors, Shadows } from '../../global/globalEnums';
-import { media_state } from '../../nodes/AudioBox';
import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
import { DocumentView } from '../../nodes/DocumentView';
import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
@@ -46,7 +44,7 @@ export class CollectionLinearView extends CollectionSubView() {
componentDidMount() {
this._widthDisposer = reaction(
- () => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * this.rootDoc[HeightSym]() : 10),
+ () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[WidthSym]() || this.dimension()) + tot + 4, 0) : 0),
width => this.childDocs.length && (this.layoutDoc._width = width),
{ fireImmediately: true }
);
@@ -80,7 +78,7 @@ export class CollectionLinearView extends CollectionSubView() {
}
};
- dimension = () => NumCast(this.rootDoc._height); // 2 * the padding
+ dimension = () => NumCast(this.rootDoc._height);
getTransform = (ele: Opt<HTMLDivElement>) => {
if (!ele) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(ele);
@@ -180,10 +178,10 @@ export class CollectionLinearView extends CollectionSubView() {
ref={r => (dref = r || undefined)}
style={{
pointerEvents: 'all',
- width: nested ? undefined : NumCast(doc._width),
- height: nested ? undefined : NumCast(doc._height),
- marginLeft: !nested ? 2.5 : 0,
- marginRight: !nested ? 2.5 : 0,
+ width: NumCast(doc._width),
+ height: NumCast(doc._height),
+ marginLeft: 2,
+ marginRight: 2,
// width: NumCast(pair.layout._width),
// height: NumCast(pair.layout._height),
}}>
@@ -199,7 +197,7 @@ export class CollectionLinearView extends CollectionSubView() {
rootSelected={this.props.isSelected}
removeDocument={this.props.removeDocument}
ScreenToLocalTransform={docXf}
- PanelWidth={nested ? doc[WidthSym] : this.dimension}
+ PanelWidth={doc[WidthSym]}
PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension}
renderDepth={this.props.renderDepth + 1}
dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)}
@@ -236,7 +234,7 @@ export class CollectionLinearView extends CollectionSubView() {
return (
<div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}>
- <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu}>
+ <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}>
{!this.props.Document.linearViewExpandable ? null : (
<Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top">
{menuOpener}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 88d045fa7..b73b1d779 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -2,14 +2,13 @@ import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
-import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionMulticolumnView.scss';
import ResizeBar from './MulticolumnResizer';
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index f18917bef..0cca83803 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -2,14 +2,13 @@ import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
-import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionMultirowView.scss';
import HeightLabel from './MultirowHeightLabel';
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
deleted file mode 100644
index 18ddd881b..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ /dev/null
@@ -1,683 +0,0 @@
-import React = require('react');
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import { extname } from 'path';
-import DatePicker from 'react-datepicker';
-import { CellInfo } from 'react-table';
-import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
-import { List } from '../../../../fields/List';
-import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
-import { emptyFunction, Utils } from '../../../../Utils';
-import { Docs } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
-import { KeyCodes } from '../../../util/KeyCodes';
-import { CompileScript } from '../../../util/Scripting';
-import { SearchUtil } from '../../../util/SearchUtil';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { undoBatch } from '../../../util/UndoManager';
-import '../../../views/DocumentDecorations.scss';
-import { EditableView } from '../../EditableView';
-import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss';
-import { DocumentIconContainer } from '../../nodes/DocumentIcon';
-import { OverlayView } from '../../OverlayView';
-import { CollectionView } from '../CollectionView';
-import './CollectionSchemaView.scss';
-import { OpenWhere } from '../../nodes/DocumentView';
-import { PinProps } from '../../nodes/trails';
-
-// intialize cell properties
-export interface CellProps {
- row: number;
- col: number;
- rowProps: CellInfo;
- // currently unused
- CollectionView: Opt<CollectionView>;
- // currently unused
- ContainingCollection: Opt<CollectionView>;
- Document: Doc;
- // column name
- fieldKey: string;
- // currently unused
- renderDepth: number;
- // called when a button is pressed on the node itself
- addDocTab: (document: Doc, where: OpenWhere) => boolean;
- pinToPres: (document: Doc, pinProps: PinProps) => void;
- moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- isFocused: boolean;
- changeFocusedCellByIndex: (row: number, col: number) => void;
- // set whether the cell is in the isEditing mode
- setIsEditing: (isEditing: boolean) => void;
- isEditable: boolean;
- setPreviewDoc: (doc: Doc) => void;
- setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
- getField: (row: number, col?: number) => void;
- // currnetly unused
- showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void;
-}
-
-@observer
-export class CollectionSchemaCell extends React.Component<CellProps> {
- // return a field key that is corrected for whether it COMMENT
- public static resolvedFieldKey(column: string, rowDoc: Doc) {
- const fieldKey = column;
- if (fieldKey.startsWith('*')) {
- const rootKey = fieldKey.substring(1);
- const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))];
- const matchedKeys = allKeys.filter(key => key.includes(rootKey));
- if (matchedKeys.length) return matchedKeys[0];
- }
- return fieldKey;
- }
- @observable protected _isEditing: boolean = false;
- protected _focusRef = React.createRef<HTMLDivElement>();
- protected _rowDoc = this.props.rowProps.original;
- // Gets the serialized data in proto form of the base proto that this document's proto inherits from
- protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
- // methods for dragging and dropping
- protected _dropDisposer?: DragManager.DragDropDisposer;
- @observable contents: string = '';
-
- componentDidMount() {
- document.addEventListener('keydown', this.onKeyDown);
- }
- componentWillUnmount() {
- document.removeEventListener('keydown', this.onKeyDown);
- }
-
- @action
- onKeyDown = (e: KeyboardEvent): void => {
- // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
- if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
- document.removeEventListener('keydown', this.onKeyDown);
- this._isEditing = true;
- this.props.setIsEditing(true);
- }
- };
-
- @action
- isEditingCallback = (isEditing: boolean): void => {
- // a general method that takes a boolean that determines whether the cell should be in
- // is-editing mode
- // remove the event listener if it's there
- document.removeEventListener('keydown', this.onKeyDown);
- // it's not already in is-editing mode, re-add the event listener
- isEditing && document.addEventListener('keydown', this.onKeyDown);
- this._isEditing = isEditing;
- this.props.setIsEditing(isEditing);
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- };
-
- @action
- onPointerDown = async (e: React.PointerEvent): Promise<void> => {
- // pan to the cell
- this.onItemDown(e);
- // focus on it
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- this.props.setPreviewDoc(this.props.rowProps.original);
-
- let url: string;
- if ((url = StrCast(this.props.rowProps.row.href))) {
- // opens up the the doc in a new window, blurring the old one
- try {
- new URL(url);
- const temp = window.open(url)!;
- temp.blur();
- window.focus();
- } catch {}
- }
-
- const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null);
- doc && this.props.setPreviewDoc(doc);
- };
-
- @undoBatch
- applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
- // apply a specified change to the cell
- const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
- if (!res.success) return false;
- // change what is rendered to this new changed cell content
- doc[this.renderFieldKey] = res.result;
- return true;
- // return whether the change was successful
- };
-
- private drop = (e: Event, de: DragManager.DropEvent) => {
- // if the drag has data at its completion
- if (de.complete.docDragData) {
- // if only one doc was dragged
- if (de.complete.docDragData.draggedDocuments.length === 1) {
- // update the renderFieldKey
- this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
- } else {
- // create schema document reflecting the new column arrangement
- const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {});
- this._rowDataDoc[this.renderFieldKey] = coll;
- }
- e.stopPropagation();
- }
- };
-
- protected dropRef = (ele: HTMLElement | null) => {
- // if the drop disposer is not undefined, run its function
- this._dropDisposer?.();
- // if ele is not null, give ele a non-undefined drop disposer
- ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
- };
-
- returnHighlights(contents: string, positions?: number[]) {
- if (positions) {
- const results = [];
- StrCast(this.props.Document._searchString);
- const length = StrCast(this.props.Document._searchString).length;
- const color = contents ? 'black' : 'grey';
-
- results.push(
- <span key="-1" style={{ color }}>
- {contents?.slice(0, positions[0])}
- </span>
- );
- positions.forEach((num, cur) => {
- results.push(
- <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}>
- {contents?.slice(num, num + length)}
- </span>
- );
- let end = 0;
- cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]);
- results.push(
- <span key={'end' + cur} style={{ color }}>
- {contents?.slice(num + length, end)}
- </span>
- );
- });
- return results;
- }
- return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>;
- }
-
- @computed get renderFieldKey() {
- // gets the resolved field key of this cell
- return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original);
- }
-
- onItemDown = async (e: React.PointerEvent) => {
- // if the document is a document used to change UI for search results in schema view
- if (this.props.Document._searchDoc) {
- const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
- const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
- // Jump to the this document
- DocumentManager.Instance.showDocument(this._rowDoc, { willPan: true }, () => this.props.setPreviewDoc(this._rowDoc));
- }
- };
-
- renderCellWithType(type: string | undefined) {
- const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-
- // the column
- const fieldKey = this.renderFieldKey;
- // the exact cell
- const field = this._rowDoc[fieldKey];
-
- const onPointerEnter = (e: React.PointerEvent): void => {
- // e.buttons === 1 means the left moue pointer is down
- if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) {
- dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over';
- }
- };
- const onPointerLeave = (e: React.PointerEvent): void => {
- // change the class name to indicate that the cell is no longer being dragged
- dragRef.current!.className = 'collectionSchemaView-cellContainer';
- };
-
- let contents = Field.toString(field as Field);
- // display 2 hyphens instead of a blank box for empty cells
- contents = contents === '' ? '--' : contents;
-
- // classname reflects the tatus of the cell
- let className = 'collectionSchemaView-cellWrapper';
- if (this._isEditing) className += ' editing';
- if (this.props.isFocused && this.props.isEditable) className += ' focused';
- if (this.props.isFocused && !this.props.isEditable) className += ' inactive';
-
- const positions = [];
- if (StrCast(this.props.Document._searchString).toLowerCase() !== '') {
- // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
- let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase();
- const search = StrCast(this.props.Document._searchString).toLowerCase();
- let start = term.indexOf(search);
- let tally = 0;
- // if search is found in term
- if (start !== -1) {
- positions.push(start);
- }
- // if search is found in term, continue finding all instances of search in term
- while (start < contents?.length && start !== -1) {
- term = term.slice(start + search.length + 1);
- tally += start + search.length + 1;
- start = term.indexOf(search);
- positions.push(tally + start);
- }
- // remove the last position
- if (positions.length > 1) {
- positions.pop();
- }
- }
- const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined';
- return (
- <div
- className="collectionSchemaView-cellContainer"
- style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }}
- ref={dragRef}
- onPointerDown={this.onPointerDown}
- onClick={action(e => (this._isEditing = true))}
- onPointerEnter={onPointerEnter}
- onPointerLeave={onPointerLeave}>
- <div className={className} ref={this._focusRef} tabIndex={-1}>
- <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}>
- {!this.props.Document._searchDoc ? (
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={'inline'}
- contents={contents}
- height={'auto'}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- placeholder={placeholder}
- GetValue={() => {
- const cfield = ComputedField.WithoutComputed(() => FieldValue(field));
- const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
- const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1];
- return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : '';
- }}
- SetValue={action((value: string) => {
- // sets what is displayed after the user makes an input
- let retVal = false;
- if (value.startsWith(':=') || value.startsWith('=:=')) {
- // decides how to compute a value when given either of the above strings
- const script = value.substring(value.startsWith('=:=') ? 3 : 2);
- retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
- } else {
- // check if the input is a number
- let inputIsNum = true;
- for (const s of value) {
- if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) {
- inputIsNum = false;
- }
- }
- // check if the input is a boolean
- const inputIsBool: boolean = value === 'false' || value === 'true';
- // what to do in the case
- if (!inputIsNum && !inputIsBool && !value.startsWith('=')) {
- // if it's not a number, it's a string, and should be processed as such
- // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically
- // after each edit
- let valueSansQuotes = value;
- if (this._isEditing) {
- const vsqLength = valueSansQuotes.length;
- // get rid of outer quotes
- valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength);
- }
- let inputAsString = '"';
- // escape any quotes in the string
- for (const i of valueSansQuotes) {
- if (i === '"') {
- inputAsString += '\\"';
- } else {
- inputAsString += i;
- }
- }
- // add a closing quote
- inputAsString += '"';
- //two options here: we can strip off outer quotes or we can figure out what's going on with the script
- const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
- const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
- // change it if a change is made, otherwise, just compile using the old cell conetnts
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- // handle numbers and expressions
- } else if (inputIsNum || value.startsWith('=')) {
- //TODO: make accept numbers
- const inputscript = value.substring(value.startsWith('=') ? 1 : 0);
- // if commas are not stripped, the parser only considers the numbers after the last comma
- let inputSansCommas = '';
- for (const s of inputscript) {
- if (!(s === ',')) {
- inputSansCommas += s;
- }
- }
- const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
- const changeMade = value.length - 2 !== value.length;
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- // handle booleans
- } else if (inputIsBool) {
- const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
- const changeMade = value.length - 2 !== value.length;
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- }
- }
- if (retVal) {
- this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing'
- this.props.setIsEditing(false);
- }
- return retVal;
- })}
- OnFillDown={async (value: string) => {
- // computes all of the value preceded by :=
- const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
- script.compiled &&
- DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) =>
- value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)
- );
- }}
- />
- ) : (
- this.returnHighlights(contents, positions)
- )}
- </div>
- </div>
- </div>
- );
- }
-
- render() {
- return this.renderCellWithType(undefined);
- }
-}
-
-@observer
-export class CollectionSchemaNumberCell extends CollectionSchemaCell {
- render() {
- return this.renderCellWithType('number');
- }
-}
-
-@observer
-export class CollectionSchemaBooleanCell extends CollectionSchemaCell {
- render() {
- return this.renderCellWithType('boolean');
- }
-}
-
-@observer
-export class CollectionSchemaStringCell extends CollectionSchemaCell {
- render() {
- return this.renderCellWithType('string');
- }
-}
-
-@observer
-export class CollectionSchemaDateCell extends CollectionSchemaCell {
- @computed get _date(): Opt<DateField> {
- // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
- return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined;
- }
-
- @action
- handleChange = (date: any) => {
- // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
- // if (script.compiled) {
- // this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
- // } else {
- // ^ DateCast is always undefined for some reason, but that is what the field should be set to
- this._rowDoc[this.renderFieldKey] = new DateField(date as Date);
- //}
- };
-
- render() {
- return !this.props.isFocused ? (
- <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : '--'}</span>
- ) : (
- <DatePicker selected={this._date?.date || new Date()} onSelect={date => this.handleChange(date)} onChange={date => this.handleChange(date)} />
- );
- }
-}
-
-@observer
-export class CollectionSchemaDocCell extends CollectionSchemaCell {
- _overlayDisposer?: () => void;
-
- @computed get _doc() {
- return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc));
- }
-
- @action
- onSetValue = (value: string) => {
- this._doc && (Doc.GetProto(this._doc).title = value);
-
- const script = CompileScript(value, {
- addReturn: true,
- typecheck: true,
- transformer: DocumentIconContainer.getTransformer(),
- });
- // compile the script
- const results = script.compiled && script.run();
- // if the script was compiled and run
- if (results && results.success) {
- this._rowDoc[this.renderFieldKey] = results.result;
- return true;
- }
- return false;
- };
-
- componentWillUnmount() {
- this.onBlur();
- }
-
- onBlur = () => {
- this._overlayDisposer?.();
- };
- onFocus = () => {
- this.onBlur();
- this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- };
-
- @action
- isEditingCallback = (isEditing: boolean): void => {
- // the isEditingCallback from a general CollectionSchemaCell
- document.removeEventListener('keydown', this.onKeyDown);
- isEditing && document.addEventListener('keydown', this.onKeyDown);
- this._isEditing = isEditing;
- this.props.setIsEditing(isEditing);
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- };
-
- render() {
- // if there's a doc, render it
- return !this._doc ? (
- this.renderCellWithType('document')
- ) : (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <div className="collectionSchemaView-cellContents-document" style={{ padding: '5.9px' }} ref={this.dropRef} onFocus={this.onFocus} onBlur={this.onBlur}>
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={'inline'}
- contents={this._doc.title || '--'}
- height={'auto'}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- GetValue={() => StrCast(this._doc?.title)}
- SetValue={action((value: string) => {
- this.onSetValue(value);
- return true;
- })}
- />
- </div>
- <div onClick={() => this._doc && this.props.addDocTab(this._doc, OpenWhere.addRight)} className="collectionSchemaView-cellContents-docButton">
- <FontAwesomeIcon icon="external-link-alt" size="lg" />
- </div>
- </div>
- );
- }
-}
-
-@observer
-export class CollectionSchemaImageCell extends CollectionSchemaCell {
- choosePath(url: URL) {
- if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
-
- const ext = extname(url.href);
- return url.href.replace(ext, '_o' + ext);
- }
-
- render() {
- const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
- const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
- const altpaths = alts
- .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
- .filter(url => url)
- .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
- const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
- // If there is a path, follow it; otherwise, follow a link to a default image icon
- const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
-
- const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
- let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
- const height = Math.min(75, width / aspect); // get a height either proportional to that or 75 px
- width = height * aspect; // increase the width of the image if necessary to maintain proportionality
-
- const reference = React.createRef<HTMLDivElement>();
- return (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
- <img src={url[0]} width={paths.length ? width : '20px'} height={paths.length ? height : '20px'} />
- </div>
- </div>
- );
- }
-}
-
-@observer
-export class CollectionSchemaListCell extends CollectionSchemaCell {
- _overlayDisposer?: () => void;
-
- @computed get _field() {
- return this._rowDoc[this.renderFieldKey];
- }
- @computed get _optionsList() {
- return this._field as List<any>;
- }
- @observable private _opened = false; // whether the list is opened
- @observable private _text = 'select an item';
- @observable private _selectedNum = 0; // the index of the list item selected
-
- @action
- onSetValue = (value: string) => {
- // change if it's a document
- this._optionsList[this._selectedNum] = this._text = value;
-
- (this._field as List<any>).splice(this._selectedNum, 1, value);
- };
-
- @action
- onSelected = (element: string, index: number) => {
- // if an item is selected, the private variables should update to reflect this
- this._text = element;
- this._selectedNum = index;
- };
-
- onFocus = () => {
- this._overlayDisposer?.();
- this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- };
-
- render() {
- const link = false;
- const reference = React.createRef<HTMLDivElement>();
-
- // if the list is not opened, don't display it; otherwise, do.
- if (this._optionsList?.length) {
- const options = !this._opened ? null : (
- <div>
- {this._optionsList.map((element, index) => {
- const val = Field.toString(element);
- return (
- <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: '6px' }} onPointerDown={e => this.onSelected(StrCast(element), index)}>
- {val}
- </div>
- );
- })}
- </div>
- );
-
- const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>;
- const textarea = (
- <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}>
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={'inline'}
- contents={this._text}
- height={'auto'}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- GetValue={() => this._text}
- SetValue={action((value: string) => {
- // add special for params
- this.onSetValue(value);
- return true;
- })}
- />
- </div>
- );
-
- //☰
- return (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
- <div className="collectionSchemaView-dropDownWrapper">
- <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: 'length', position: 'relative' }} onClick={action(e => (this._opened = !this._opened))}>
- <FontAwesomeIcon icon={this._opened ? 'caret-up' : 'caret-down'} size="sm" />
- </button>
- <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div>
- </div>
- {options}
- </div>
- </div>
- );
- }
- return this.renderCellWithType('list');
- }
-}
-
-@observer
-export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
- @computed get _isChecked() {
- return BoolCast(this._rowDoc[this.renderFieldKey]);
- }
-
- render() {
- const reference = React.createRef<HTMLDivElement>();
- return (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} />
- </div>
- );
- }
-}
-
-@observer
-export class CollectionSchemaButtons extends CollectionSchemaCell {
- // the navigation buttons for schema view when it is used for search.
- render() {
- return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? (
- <></>
- ) : (
- <div style={{ paddingTop: 8, paddingLeft: 3 }}>
- <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}>
- <FontAwesomeIcon icon="arrow-up" size="sm" />
- </button>
- <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}>
- <FontAwesomeIcon icon="arrow-down" size="sm" />
- </button>
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
deleted file mode 100644
index 9653f2808..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ /dev/null
@@ -1,513 +0,0 @@
-import React = require("react");
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction, trace } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc";
-import { listSpec } from "../../../../fields/Schema";
-import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { ScriptField } from "../../../../fields/ScriptField";
-import { Cast, StrCast } from "../../../../fields/Types";
-import { undoBatch } from "../../../util/UndoManager";
-import { CollectionView } from "../CollectionView";
-import { ColumnType } from "./CollectionSchemaView";
-import "./CollectionSchemaView.scss";
-
-const higflyout = require("@hig/flyout");
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-
-export interface AddColumnHeaderProps {
- createColumn: () => void;
-}
-
-@observer
-export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
- // the button that allows the user to add a column
- render() {
- return <button className="add-column" onClick={() => this.props.createColumn()}>
- <FontAwesomeIcon icon="plus" size="sm" />
- </button>;
- }
-}
-
-export interface ColumnMenuProps {
- columnField: SchemaHeaderField;
- // keyValue: string;
- possibleKeys: string[];
- existingKeys: string[];
- // keyType: ColumnType;
- typeConst: boolean;
- menuButtonContent: JSX.Element;
- addNew: boolean;
- onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
- setIsEditing: (isEditing: boolean) => void;
- deleteColumn: (column: string) => void;
- onlyShowOptions: boolean;
- setColumnType: (column: SchemaHeaderField, type: ColumnType) => void;
- setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void;
- anchorPoint?: any;
- setColumnColor: (column: SchemaHeaderField, color: string) => void;
-}
-@observer
-export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {
- @observable private _isOpen: boolean = false;
- @observable private _node: HTMLDivElement | null = null;
-
- componentDidMount() { document.addEventListener("pointerdown", this.detectClick); }
-
- componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); }
-
- @action
- detectClick = (e: PointerEvent) => {
- !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false);
- }
-
- @action
- toggleIsOpen = (): void => {
- this.props.setIsEditing(this._isOpen = !this._isOpen);
- }
-
- changeColumnType = (type: ColumnType) => {
- this.props.setColumnType(this.props.columnField, type);
- }
-
- changeColumnSort = (desc: boolean | undefined) => {
- this.props.setColumnSort(this.props.columnField, desc);
- }
-
- changeColumnColor = (color: string) => {
- this.props.setColumnColor(this.props.columnField, color);
- }
-
- @action
- setNode = (node: HTMLDivElement): void => {
- if (node) {
- this._node = node;
- }
- }
-
- renderTypes = () => {
- if (this.props.typeConst) return (null);
-
- const type = this.props.columnField.type;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Column type:</label>
- <div className="columnMenu-types">
- <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}>
- <FontAwesomeIcon icon={"align-justify"} size="sm" />
- Any
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}>
- <FontAwesomeIcon icon={"hashtag"} size="sm" />
- Number
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}>
- <FontAwesomeIcon icon={"font"} size="sm" />
- Text
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}>
- <FontAwesomeIcon icon={"check-square"} size="sm" />
- Checkbox
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}>
- <FontAwesomeIcon icon={"list-ul"} size="sm" />
- List
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}>
- <FontAwesomeIcon icon={"file"} size="sm" />
- Document
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}>
- <FontAwesomeIcon icon={"image"} size="sm" />
- Image
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}>
- <FontAwesomeIcon icon={"calendar"} size="sm" />
- Date
- </div>
- </div>
- </div >
- );
- }
-
- renderSorting = () => {
- const sort = this.props.columnField.desc;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Sort by:</label>
- <div className="columnMenu-sort">
- <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}>
- <FontAwesomeIcon icon="sort-amount-down" size="sm" />
- Sort descending
- </div>
- <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}>
- <FontAwesomeIcon icon="sort-amount-up" size="sm" />
- Sort ascending
- </div>
- <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}>
- <FontAwesomeIcon icon="times" size="sm" />
- Clear sorting
- </div>
- </div>
- </div>
- );
- }
-
- renderColors = () => {
- const selected = this.props.columnField.color;
-
- const pink = PastelSchemaPalette.get("pink2");
- const purple = PastelSchemaPalette.get("purple2");
- const blue = PastelSchemaPalette.get("bluegreen1");
- const yellow = PastelSchemaPalette.get("yellow4");
- const red = PastelSchemaPalette.get("red2");
- const gray = "#f1efeb";
-
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Color:</label>
- <div className="columnMenu-colors">
- <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
- </div>
- </div>
- );
- }
-
- renderContent = () => {
- return (
- <div className="collectionSchema-header-menuOptions">
- {this.props.onlyShowOptions ? <></> :
- <>
- {this.renderTypes()}
- {this.renderSorting()}
- {this.renderColors()}
- <div className="collectionSchema-headerMenu-group">
- <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button>
- </div>
- </>
- }
- </div>
- );
- }
-
- render() {
- return (
- <div className="collectionSchema-header-menu" ref={this.setNode}>
- <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}>
- <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div>
- </ Flyout >
- </div>
- );
- }
-}
-
-
-export interface KeysDropdownProps {
- keyValue: string;
- possibleKeys: string[];
- existingKeys: string[];
- canAddNew: boolean;
- addNew: boolean;
- onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void;
- setIsEditing: (isEditing: boolean) => void;
- width?: string;
- docs?: Doc[];
- Document: Doc;
- dataDoc: Doc | undefined;
- fieldKey: string;
- ContainingCollectionDoc: Doc | undefined;
- ContainingCollectionView: Opt<CollectionView>;
- active?: (outsideReaction?: boolean) => boolean | undefined;
- openHeader: (column: any, screenx: number, screeny: number) => void;
- col: SchemaHeaderField;
- icon: IconProp;
-}
-@observer
-export class KeysDropdown extends React.Component<KeysDropdownProps> {
- @observable private _key: string = this.props.keyValue;
- @observable private _searchTerm: string = this.props.keyValue + ":";
- @observable private _isOpen: boolean = false;
- @observable private _node: HTMLDivElement | null = null;
- @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef();
-
- @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
- @action setKey = (key: string): void => { this._key = key; };
- @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; };
-
- @action
- onSelect = (key: string): void => {
- this.props.onSelect(this._key, key, this.props.addNew);
- this.setKey(key);
- this._isOpen = false;
- this.props.setIsEditing(false);
- }
-
- @action
- setNode = (node: HTMLDivElement): void => {
- if (node) {
- this._node = node;
- }
- }
-
- componentDidMount() {
- document.addEventListener("pointerdown", this.detectClick);
- const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters?.some(filter => filter.split(":")[0] === this._key)) {
- runInAction(() => this.closeResultsVisibility = "contents");
- }
- }
-
- @action
- detectClick = (e: PointerEvent): void => {
- if (this._node && this._node.contains(e.target as Node)) {
- } else {
- this._isOpen = false;
- this.props.setIsEditing(false);
- }
- }
-
- private tempfilter: string = "";
- @undoBatch
- onKeyDown = (e: React.KeyboardEvent): void => {
- if (e.key === "Enter") {
- e.stopPropagation();
- if (this._searchTerm.includes(":")) {
- const colpos = this._searchTerm.indexOf(":");
- const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
- if (temp === "") {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.updateFilter();
- }
- else {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.tempfilter = temp;
- Doc.setDocFilter(this.props.Document, this._key, temp, "check");
- this.props.col.setColor("green");
- this.closeResultsVisibility = "contents";
- }
- }
- else {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.updateFilter();
- if (this.showKeys.length) {
- this.onSelect(this.showKeys[0]);
- } else if (this._searchTerm !== "" && this.props.canAddNew) {
- this.setSearchTerm(this._searchTerm || this._key);
- this.onSelect(this._searchTerm);
- }
- }
- }
- }
-
- onChange = (val: string): void => {
- this.setSearchTerm(val);
- }
-
- @action
- onFocus = (e: React.FocusEvent): void => {
- this._isOpen = true;
- this.props.setIsEditing(true);
- }
-
- @computed get showKeys() {
- const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"];
- const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
- const showKeys = new Set<string>();
- [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.noviceMode ||
- whitelistKeys.includes(key)
- || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null);
- return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm));
- }
-
- @computed get renderOptions() {
- if (!this._isOpen) {
- this.defaultMenuHeight = 0;
- return (null);
- }
- const options = this.showKeys.map(key => {
- return <div key={key} className="key-option" style={{
- border: "1px solid lightgray",
- width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
- }}
- onPointerDown={e => {
- e.stopPropagation();
- }}
- onClick={() => {
- this.onSelect(key);
- this.setSearchTerm("");
- }}>{key}</div>;
- });
-
- // if search term does not already exist as a group type, give option to create new group type
-
- if (this._key !== this._searchTerm.slice(0, this._key.length)) {
- if (this._searchTerm !== "" && this.props.canAddNew) {
- options.push(<div key={""} className="key-option" style={{
- border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
- }}
- onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}>
- Create "{this._searchTerm}" key</div>);
- }
- }
-
- if (options.length === 0) {
- this.defaultMenuHeight = 0;
- }
- else {
- if (this.props.docs) {
- const panesize = this.props.docs.length * 30;
- options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
- }
- else {
- options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
- }
- }
- return options;
- }
-
- @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); }
-
- @computed get renderFilterOptions() {
- if (!this._isOpen || !this.props.dataDoc) {
- this.defaultMenuHeight = 0;
- return (null);
- }
- const keyOptions: string[] = [];
- const colpos = this._searchTerm.indexOf(":");
- const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
- this.docSafe.forEach(doc => {
- const key = StrCast(doc[this._key]);
- if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") {
- keyOptions.push(key);
- }
- });
-
- const filters = StrListCast(this.props.Document._docFilters);
- if (filters.some(filter => filter.split(":")[0] === this._key) === false) {
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
- if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) {
- keyOptions.push(filters[i + 1]);
- }
- }
- const options = keyOptions.map(key => {
- let bool = false;
- if (filters !== undefined) {
- const ind = filters.findIndex(filter => filter.split(":")[1] === key);
- const fields = ind === -1 ? undefined : filters[ind].split(":");
- bool = fields ? fields[2] === "check" : false;
- }
- return <div key={key} className="key-option" style={{
- paddingLeft: 5, textAlign: "left",
- width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white",
- }}
- >
- <input type="checkbox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}
- onChange={action(e => {
- if (e.target.checked) {
- Doc.setDocFilter(this.props.Document, this._key, key, "check");
- this.closeResultsVisibility = "contents";
- this.props.col.setColor("green");
- } else {
- Doc.setDocFilter(this.props.Document, this._key, key, "remove");
- this.updateFilter();
- }
- })}
- checked={bool}
- />
- <span style={{ paddingLeft: 4 }}>
- {key}
- </span>
-
- </div>;
- });
- if (options.length === 0) {
- this.defaultMenuHeight = 0;
- }
- else {
- if (this.props.docs) {
- const panesize = this.props.docs.length * 30;
- options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
- }
- else {
- options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
- }
-
- }
- return options;
- }
-
- @observable defaultMenuHeight = 0;
-
-
- updateFilter() {
- const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- }
-
- @computed get scriptField() {
- const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)";
- const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
- return script ? () => script : undefined;
- }
- filterBackground = () => "rgba(105, 105, 105, 0.432)";
- @observable filterOpen: boolean | undefined = undefined;
- closeResultsVisibility: string = "none";
-
- removeFilters = (e: React.PointerEvent): void => {
- const keyOptions: string[] = [];
- this.docSafe.forEach(doc => {
- const key = StrCast(doc[this._key]);
- if (keyOptions.includes(key) === false) {
- keyOptions.push(key);
- }
- });
-
- Doc.setDocFilter(this.props.Document, this._key, "", "remove");
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- render() {
- return (
- <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}>
- <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}>
- <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} />
- </div>
-
- <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}>
- <input className="keys-search" style={{ width: "100%" }}
- ref={this._inputRef} type="text"
- value={this._searchTerm} placeholder="Column key"
- onKeyDown={this.onKeyDown}
- onChange={e => this.onChange(e.target.value)}
- onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }}
- onFocus={this.onFocus} ></input>
- <div style={{ display: this.closeResultsVisibility }}>
- <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg"
- style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} />
- </div>
- {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{
- width: this.props.width, maxWidth: this.props.width, height: "auto",
- }}>
- {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions}
- </div>}
- </div >
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
deleted file mode 100644
index 28d2e6ab1..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React = require('react');
-import { action } from 'mobx';
-import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { DragManager } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import './CollectionSchemaView.scss';
-
-export interface MovableColumnProps {
- columnRenderer: React.ReactNode;
- columnValue: SchemaHeaderField;
- allColumns: SchemaHeaderField[];
- reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void;
- ScreenToLocalTransform: () => Transform;
-}
-export class MovableColumn extends React.Component<MovableColumnProps> {
- // The header of the column
- private _header?: React.RefObject<HTMLDivElement> = React.createRef();
- // The container of the function that is responsible for moving the column over to a new plac
- private _colDropDisposer?: DragManager.DragDropDisposer;
- // initial column position
- private _startDragPosition: { x: number; y: number } = { x: 0, y: 0 };
- // sensitivity to being dragged, in pixels
- private _sensitivity: number = 16;
- // Column reference ID
- private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-
- onPointerEnter = (e: React.PointerEvent): void => {
- // if the column is left-clicked and it is being dragged
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- document.addEventListener('pointermove', this.onDragMove, true);
- }
- };
-
- onPointerLeave = (e: React.PointerEvent): void => {
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- document.removeEventListener('pointermove', this.onDragMove, true);
- !e.buttons && document.removeEventListener('pointermove', this.onPointerMove);
- };
-
- onDragMove = (e: PointerEvent): void => {
- // only take into account the horizonal direction when a column is dragged
- const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
- const rect = this._header!.current!.getBoundingClientRect();
- // Now store the point at the top center of the column when it was in its original position
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
- // to be compared with its new horizontal position
- const before = x[0] < bounds[0];
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- if (before) this._header!.current!.className += ' col-before';
- if (!before) this._header!.current!.className += ' col-after';
- e.stopPropagation();
- };
-
- createColDropTarget = (ele: HTMLDivElement) => {
- this._colDropDisposer?.();
- if (ele) {
- this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this));
- }
- };
-
- colDrop = (e: Event, de: DragManager.DropEvent) => {
- document.removeEventListener('pointermove', this.onDragMove, true);
- // we only care about whether the column is shifted to the side
- const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- // get the dimensions of the smallest rectangle that bounds the header
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
- // get whether the column was dragged before or after where it is now
- const before = x[0] < bounds[0];
- const colDragData = de.complete.columnDragData;
- // if there is colDragData, which happen when the drag is complete, reorder the columns according to the established variables
- if (colDragData) {
- e.stopPropagation();
- this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
- return true;
- }
- return false;
- };
-
- onPointerMove = (e: PointerEvent) => {
- const onRowMove = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
-
- document.removeEventListener('pointermove', onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- const dragData = new DragManager.ColumnDragData(this.props.columnValue);
- DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y);
- };
- const onRowUp = (): void => {
- document.removeEventListener('pointermove', onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- };
- // if the left mouse button is the one being held
- if (e.buttons === 1) {
- const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
- // If the movemnt of the drag exceeds the sensitivity value
- if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
- document.removeEventListener('pointermove', this.onPointerMove);
- e.stopPropagation();
-
- document.addEventListener('pointermove', onRowMove);
- document.addEventListener('pointerup', onRowUp);
- }
- }
- };
-
- onPointerUp = (e: React.PointerEvent) => {
- document.removeEventListener('pointermove', this.onPointerMove);
- };
-
- @action
- onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
- this._dragRef = ref;
- const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
- // If the cell thing dragged is not being edited
- if (!(e.target as any)?.tagName.includes('INPUT')) {
- this._startDragPosition = { x: dx, y: dy };
- document.addEventListener('pointermove', this.onPointerMove);
- }
- };
-
- render() {
- const reference = React.createRef<HTMLDivElement>();
-
- return (
- <div className="collectionSchema-col" ref={this.createColDropTarget}>
- <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- <div className="col-dragger" ref={reference} onPointerDown={e => this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}>
- {this.props.columnRenderer}
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
deleted file mode 100644
index 3cb2df7d3..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action } from 'mobx';
-import * as React from 'react';
-import { ReactTableDefaults, RowInfo } from 'react-table';
-import { Doc } from '../../../../fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../../../fields/Types';
-import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType, SetupDrag } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
-import { ContextMenu } from '../../ContextMenu';
-import { OpenWhere } from '../../nodes/DocumentView';
-import './CollectionSchemaView.scss';
-
-export interface MovableRowProps {
- rowInfo: RowInfo;
- ScreenToLocalTransform: () => Transform;
- addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
- removeDoc: (doc: Doc | Doc[]) => boolean;
- rowFocused: boolean;
- textWrapRow: (doc: Doc) => void;
- rowWrapped: boolean;
- dropAction: string;
- addDocTab: any;
-}
-
-export class MovableRow extends React.Component<React.PropsWithChildren<MovableRowProps>> {
- private _header?: React.RefObject<HTMLDivElement> = React.createRef();
- private _rowDropDisposer?: DragManager.DragDropDisposer;
-
- // Event listeners are only necessary when the user is hovering over the table
- // Create one when the mouse starts hovering...
- onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- document.addEventListener('pointermove', this.onDragMove, true);
- }
- };
- // ... and delete it when the mouse leaves
- onPointerLeave = (e: React.PointerEvent): void => {
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- document.removeEventListener('pointermove', this.onDragMove, true);
- };
- // The method for the event listener, reorders columns when dragged to their new locations.
- onDragMove = (e: PointerEvent): void => {
- const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
- const before = x[1] < bounds[1];
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- if (before) this._header!.current!.className += ' row-above';
- if (!before) this._header!.current!.className += ' row-below';
- e.stopPropagation();
- };
- componentWillUnmount() {
- this._rowDropDisposer?.();
- }
- //
- createRowDropTarget = (ele: HTMLDivElement) => {
- this._rowDropDisposer?.();
- if (ele) {
- this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
- }
- };
- // Controls what hppens when a row is dragged and dropped
- rowDrop = (e: Event, de: DragManager.DropEvent) => {
- this.onPointerLeave(e as any);
- const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
- if (!rowDoc) return false;
-
- const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
- const before = x[1] < bounds[1];
-
- const docDragData = de.complete.docDragData;
- if (docDragData) {
- e.stopPropagation();
- if (docDragData.draggedDocuments[0] === rowDoc) return true;
- const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before);
- const movedDocs = docDragData.draggedDocuments;
- return docDragData.dropAction || docDragData.userDropAction
- ? docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
- : docDragData.moveDocument
- ? movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false)
- : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false);
- }
- return false;
- };
-
- onRowContextMenu = (e: React.MouseEvent): void => {
- const description = this.props.rowWrapped ? 'Unwrap text on row' : 'Text wrap row';
- ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: 'file-pdf' });
- };
-
- @undoBatch
- @action
- move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => {
- const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection);
- return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
- };
-
- @action
- onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- console.log('yes');
- if (e.key === 'Backspace' || e.key === 'Delete') {
- undoBatch(() => this.props.removeDoc(this.props.rowInfo.original));
- }
- };
-
- render() {
- const { children = null, rowInfo } = this.props;
-
- if (!rowInfo) {
- return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>;
- }
-
- const { original } = rowInfo;
- const doc = FieldValue(Cast(original, Doc));
-
- if (!doc) return null;
-
- const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType);
-
- let className = 'collectionSchema-row';
- if (this.props.rowFocused) className += ' row-focused';
- if (this.props.rowWrapped) className += ' row-wrapped';
-
- return (
- <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}>
- <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown}>
- <div className="row-dragger">
- <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}>
- <FontAwesomeIcon icon="trash" size="sm" />
- </div>
- <div className="row-option" style={{ cursor: 'grab' }} ref={reference} onPointerDown={onItemDown}>
- <FontAwesomeIcon icon="grip-vertical" size="sm" />
- </div>
- <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, OpenWhere.addRight)}>
- <FontAwesomeIcon icon="external-link-alt" size="sm" />
- </div>
- </div>
- {children}
- </ReactTableDefaults.TrComponent>
- </div>
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 19401c7f0..1ef2fb4ef 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -1,599 +1,226 @@
@import '../../global/globalCssVariables.scss';
-@import '../../../../../node_modules/react-table/react-table.css';
-.collectionSchemaView-container {
- border-width: $COLLECTION_BORDER_WIDTH;
- border-color: $medium-gray;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: relative;
- top: 0;
- width: 100%;
- height: 100%;
- margin-top: 0;
- transition: top 0.5s;
- display: flex;
- justify-content: space-between;
- flex-wrap: nowrap;
- touch-action: none;
- div {
- touch-action: none;
- }
- .collectionSchemaView-tableContainer {
- width: 100%;
- height: 100%;
- }
- .collectionSchemaView-dividerDragger {
- position: relative;
- height: 100%;
- width: $SCHEMA_DIVIDER_WIDTH;
- z-index: 20;
- right: 0;
- top: 0;
- background: gray;
- cursor: col-resize;
- }
- // .documentView-node:first-child {
- // background: $white;
- // }
-}
-.collectionSchemaView-searchContainer {
- border-width: $COLLECTION_BORDER_WIDTH;
- border-color: $medium-gray;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: relative;
- top: 0;
- width: 100%;
+.collectionSchemaView {
+ cursor: default;
height: 100%;
- margin-top: 0;
- transition: top 0.5s;
display: flex;
- justify-content: space-between;
- flex-wrap: nowrap;
- touch-action: none;
- padding: 2px;
- div {
- touch-action: none;
- }
- .collectionSchemaView-tableContainer {
- width: 100%;
- height: 100%;
- }
- .collectionSchemaView-dividerDragger {
- position: relative;
- height: 100%;
- width: 20px;
- z-index: 20;
- right: 0;
- top: 0;
- background: gray;
- cursor: col-resize;
- }
- // .documentView-node:first-child {
- // background: $white;
- // }
-}
+ flex-direction: row;
+
+ .schema-table {
+ background-color: $white;
+ cursor: grab;
+ overflow: scroll;
+
+ .schema-column-menu,
+ .schema-filter-menu {
+ background: $light-gray;
+ position: absolute;
+ min-width: 200px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ z-index: 1;
-.ReactTable {
- width: 100%;
- background: white;
- box-sizing: border-box;
- border: none !important;
- float: none !important;
- .rt-table {
- height: 100%;
- display: -webkit-inline-box;
- direction: ltr;
- overflow: visible;
- }
- .rt-noData {
- display: none;
- }
- .rt-thead {
- width: 100%;
- z-index: 100;
- overflow-y: visible;
- &.-header {
- font-size: 12px;
- height: 30px;
- box-shadow: none;
- z-index: 100;
- overflow-y: visible;
- }
- .rt-resizable-header-content {
- height: 100%;
- overflow: visible;
- }
- .rt-th {
- padding: 0;
- border-left: solid 1px $light-gray;
- }
- }
- .rt-th {
- font-size: 13px;
- text-align: center;
- &:last-child {
- overflow: visible;
- }
- }
- .rt-tbody {
- width: 100%;
- direction: rtl;
- overflow: visible;
- .rt-td {
- border-right: 1px solid rgba(0, 0, 0, 0.2);
- }
- }
- .rt-tr-group {
- direction: ltr;
- flex: 0 1 auto;
- min-height: 30px;
- border: 0 !important;
- }
- .rt-tr-group:nth-of-type(even) {
- direction: ltr;
- flex: 0 1 auto;
- min-height: 30px;
- border: 0 !important;
- background-color: red;
- }
- .rt-tr {
- width: 100%;
- min-height: 30px;
- }
- .rt-td {
- padding: 0;
- font-size: 13px;
- text-align: center;
- white-space: nowrap;
- display: flex;
- align-items: center;
- .imageBox-cont {
- position: relative;
- max-height: 100%;
- }
- .imageBox-cont img {
- object-fit: contain;
- max-width: 100%;
- height: 100%;
- }
- .videoBox-cont {
- object-fit: contain;
- width: auto;
- height: 100%;
- }
- }
- .rt-td.rt-expandable {
- display: flex;
- align-items: center;
- height: inherit;
- }
- .rt-resizer {
- width: 8px;
- right: -4px;
- }
- .rt-resizable-header {
- padding: 0;
- height: 30px;
- }
- .rt-resizable-header:last-child {
- overflow: visible;
- .rt-resizer {
- width: 5px !important;
- }
- }
-}
+ .schema-key-search-input {
+ width: calc(100% - 20px);
+ margin: 10px;
+ }
-.documentView-node-topmost {
- text-align: left;
- transform-origin: center top;
- display: inline-block;
-}
+ .schema-key-search-result {
+ cursor: pointer;
+ padding: 2px 10px;
+ width: 100%;
-.collectionSchema-col {
- height: 100%;
-}
+ &:hover {
+ background-color: $medium-gray;
+ }
+ }
-.collectionSchema-header-menu {
- height: auto;
- z-index: 100;
- position: absolute;
- background: white;
- padding: 5px;
- position: fixed;
- background: white;
- border: black 1px solid;
- .collectionSchema-header-toggler {
- z-index: 100;
- width: 100%;
- height: 100%;
- padding: 4px;
- letter-spacing: 2px;
- text-transform: uppercase;
- svg {
- margin-right: 4px;
- }
- }
-}
+ .schema-key-search,
+ .schema-new-key-options {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
-.collectionSchemaView-header {
- height: 100%;
- color: gray;
- z-index: 100;
- overflow-y: visible;
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
-}
+ .schema-new-key-options {
+ margin: 10px;
+ .schema-key-warning {
+ color: red;
+ font-weight: normal;
+ align-self: center;
+ }
+ }
-button.add-column {
- width: 28px;
-}
+ .schema-key-list {
+ width: 100%;
+ max-height: 300px;
+ overflow-y: auto;
+ }
-.collectionSchemaView-menuOptions-wrapper {
- background: rgb(241, 239, 235);
- display: flex;
- cursor: default;
- height: 100%;
- align-content: center;
- align-items: center;
-}
+ .schema-key-type-option {
+ margin: 2px 0px;
-.collectionSchema-header-menuOptions {
- color: black;
- width: 180px;
- text-align: left;
- .collectionSchema-headerMenu-group {
- padding: 7px 0;
- border-bottom: 1px solid lightgray;
- cursor: pointer;
- &:first-child {
- padding-top: 0;
- }
- &:last-child {
- border: none;
- text-align: center;
- padding: 12px 0 0 0;
- }
- }
- label {
- color: $medium-gray;
- font-weight: normal;
- letter-spacing: 2px;
- text-transform: uppercase;
- }
- input {
- color: black;
- width: 100%;
- }
- .columnMenu-option {
- cursor: pointer;
- padding: 3px;
- background-color: white;
- transition: background-color 0.2s;
- &:hover {
- background-color: $light-gray;
- }
- &.active {
- font-weight: bold;
- border: 2px solid $light-gray;
- }
- svg {
- color: gray;
- margin-right: 5px;
- width: 10px;
- }
- }
+ input {
+ margin-right: 5px;
+ }
+ }
- .keys-dropdown {
- position: relative;
- //width: 100%;
- background-color: white;
- input {
- border: 2px solid $light-gray;
- padding: 3px;
- height: 28px;
- font-weight: bold;
- letter-spacing: '2px';
- text-transform: 'uppercase';
- &:focus {
- font-weight: normal;
+ .schema-key-default-val {
+ margin: 5px 0;
}
- }
- }
- .columnMenu-colors {
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
- .columnMenu-colorPicker {
- cursor: pointer;
- width: 20px;
- height: 20px;
- border-radius: 10px;
- &.active {
- border: 2px solid white;
- box-shadow: 0 0 0 2px lightgray;
+
+ .schema-column-menu-button {
+ cursor: pointer;
+ padding: 2px 5px;
+ background: $medium-blue;
+ border-radius: 9999px;
+ color: $white;
+ width: fit-content;
+ margin: 5px;
+ align-self: center;
}
}
}
-}
-.schema-icon {
- cursor: pointer;
- width: 25px;
- height: 25px;
- display: flex;
- align-items: center;
- justify-content: center;
- align-content: center;
- background-color: $medium-blue;
- color: white;
- margin-right: 5px;
- font-size: 10px;
- border-radius: 3px;
-}
-
-.keys-options-wrapper {
- position: absolute;
- text-align: left;
- height: fit-content;
- top: 100%;
- z-index: 21;
- background-color: #ffffff;
- box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
- padding: 1px;
- .key-option {
- cursor: pointer;
- color: #000000;
- width: 100%;
- height: 25px;
- font-weight: 400;
- display: flex;
- justify-content: left;
- align-items: center;
- padding-left: 5px;
- &:hover {
- background-color: $light-gray;
- }
+ .schema-preview-divider {
+ height: 100%;
+ background: black;
+ cursor: ew-resize;
}
}
-.collectionSchema-row {
- height: 100%;
- background-color: white;
- &.row-focused .rt-td {
- background-color: $light-blue; //$light-gray;
- overflow: visible;
- }
- &.row-wrapped {
- .rt-td {
- white-space: normal;
- }
- }
- .row-dragger {
+.schema-header-row {
+ cursor: grab;
+ justify-content: flex-end;
+
+ .row-menu {
display: flex;
- justify-content: space-evenly;
- width: 58px;
- position: absolute;
- /* max-width: 50px; */
- min-height: 30px;
- align-items: center;
- color: lightgray;
- background-color: white;
- transition: color 0.1s ease;
- .row-option {
- color: black;
- cursor: pointer;
- position: relative;
- transition: color 0.1s ease;
- display: flex;
- flex-direction: column;
- justify-content: center;
- z-index: 2;
- border-radius: 3px;
- padding: 3px;
- &:hover {
- background-color: $light-gray;
- }
- }
- }
- .collectionSchema-row-wrapper {
- &.row-above {
- border-top: 1px solid $medium-blue;
- }
- &.row-below {
- border-bottom: 1px solid $medium-blue;
- }
- &.row-inside {
- border: 2px dashed $medium-blue;
- }
- .row-dragging {
- background-color: blue;
- }
+ justify-content: flex-end;
}
}
-.collectionSchemaView-cellContainer {
- width: 100%;
- height: unset;
-}
-
-.collectionSchemaView-cellContents {
- width: 100%;
-}
-
-.collectionSchemaView-cellWrapper {
+.schema-column-header {
+ background-color: $light-gray;
+ font-weight: bold;
display: flex;
- height: 100%;
- text-align: left;
- padding-left: 19px;
- position: relative;
+ flex-direction: row;
+ justify-content: space-between;
align-items: center;
- align-content: center;
- &:focus {
- outline: none;
- }
- &.editing {
- padding: 0;
- box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
- transform: scale(1.1);
- z-index: 40;
- input {
- outline: 0;
- border: none;
- background-color: $white;
- width: 100%;
- height: fit-content;
- min-height: 26px;
- }
- }
- &.focused {
+ padding: 0;
+ z-index: 1;
+ border: 1px solid $medium-gray;
+ //overflow: hidden;
+
+ .schema-column-title {
+ flex-grow: 2;
+ margin: 5px;
overflow: hidden;
- &.inactive {
- border: none;
- }
- }
- p {
- width: 100%;
- height: 100%;
- }
- &:hover .collectionSchemaView-cellContents-docExpander {
- display: block;
- }
- .collectionSchemaView-cellContents-document {
- display: inline-block;
+ min-width: 20%;
}
- .collectionSchemaView-cellContents-docButton {
- float: right;
- width: '15px';
- height: '15px';
+
+ .schema-header-menu {
+ margin: 5px;
}
- .collectionSchemaView-dropdownWrapper {
- border: grey;
- border-style: solid;
- border-width: 1px;
- height: 30px;
- .collectionSchemaView-dropdownButton {
- //display: inline-block;
- float: left;
- height: 100%;
- }
- .collectionSchemaView-dropdownText {
- display: inline-block;
- //float: right;
- height: 100%;
- display: 'flex';
- font-size: 13;
- justify-content: 'center';
- align-items: 'center';
+
+ .schema-column-resizer {
+ height: 100%;
+ width: 3px;
+ cursor: ew-resize;
+
+ &:hover {
+ background-color: $light-blue;
}
}
- .collectionSchemaView-dropdownContainer {
- position: absolute;
- border: 1px solid rgba(0, 0, 0, 0.04);
- box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14);
- .collectionSchemaView-dropdownOption:hover {
- background-color: rgba(0, 0, 0, 0.14);
- cursor: pointer;
- }
+
+ .schema-column-resizer.left {
+ min-width: 5px;
+ transform: translate(-3px, 0px);
+ align-self: flex-start;
+ background-color: $medium-gray;
}
}
-.collectionSchemaView-cellContents-docExpander {
- height: 30px;
- width: 30px;
- display: none;
- position: absolute;
- top: 0;
- right: 0;
- background-color: lightgray;
+.schema-header-menu {
+ display: flex;
+ flex-direction: row;
}
-.doc-drag-over {
- background-color: red;
+.schema-row-wrapper {
+ overflow: hidden;
}
-.collectionSchemaView-toolbar {
- z-index: 100;
+.schema-header-row {
+ background-color: $light-gray;
+ overflow: hidden;
}
-.collectionSchemaView-toolbar {
- height: 30px;
+.schema-header-row,
+.schema-row {
display: flex;
- justify-content: flex-end;
- padding: 0 10px;
- border-bottom: 2px solid gray;
- .collectionSchemaView-toolbar-item {
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
+ flex-direction: row;
+ height: 100%;
+ overflow: auto;
}
-#preview-schema-checkbox-div {
- margin-left: 20px;
- font-size: 12px;
+.schema-header-row > .schema-column-header:nth-child(2) > .left {
+ display: none;
}
-.collectionSchemaView-table {
- width: 100%;
- height: 100%;
- overflow: auto;
- padding: 3px;
+.schema-table-cell,
+.row-menu {
+ border: 1px solid $medium-gray;
+ overflow: hidden;
+ padding: 5px;
}
-.rt-td.rt-expandable {
- overflow: visible;
- position: relative;
- height: 100%;
- z-index: 1;
-}
+.schema-row {
+ cursor: grab;
+ justify-content: flex-end;
+ background: white;
-.reactTable-sub {
- background-color: rgb(252, 252, 252);
- width: 100%;
- .rt-thead {
- display: none;
- }
- .row-dragger {
- background-color: rgb(252, 252, 252);
- }
- .rt-table {
- background-color: rgb(252, 252, 252);
+ .row-menu {
+ display: flex;
+ flex-direction: row;
+ min-width: 50px;
+ justify-content: flex-end;
}
- .collectionSchemaView-table {
- width: 100%;
- border: solid 1px;
- overflow: visible;
- padding: 0px;
+
+ .row-cells {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
}
}
-.collectionSchemaView-expander {
- height: 100%;
- min-height: 30px;
- position: absolute;
- color: gray;
- width: 20;
- height: auto;
- left: 55;
+.schema-row-button,
+.schema-header-button {
+ color: $dark-gray;
+ margin: 3px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
svg {
- position: absolute;
- top: 50%;
- left: 10;
- transform: translate(-50%, -50%);
+ width: 15px;
}
}
-.collectionSchemaView-addRow {
- color: gray;
- letter-spacing: 2px;
- text-transform: uppercase;
+.schema-sort-button {
+ width: 17px;
+ height: 17px;
+ border-radius: 30%;
+ background-color: $dark-gray;
+ color: white;
+ margin: 3px;
cursor: pointer;
- font-size: 10.5px;
- margin-left: 50px;
- margin-top: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ width: 12px;
+ }
}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index c4ee1805f..fd9bcf681 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,108 +1,66 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked } from 'mobx';
+import { action, computed, observable, ObservableMap, untracked } from 'mobx';
import { observer } from 'mobx-react';
-import Measure from 'react-measure';
-import { Resize } from 'react-table';
-import { Doc, Opt } from '../../../../fields/Doc';
+import { computedFn } from 'mobx-utils';
+import { Doc, Field, StrListCast } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
-import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast, NumCast } from '../../../../fields/Types';
-import { TraceMobx } from '../../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { DocUtils } from '../../../documents/Documents';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
import { SelectionManager } from '../../../util/SelectionManager';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
-import { ContextMenuProps } from '../../ContextMenuItem';
-import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView } from '../../nodes/DocumentView';
+import { EditableView } from '../../EditableView';
+import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionSchemaView.scss';
-import { SchemaTable } from './SchemaTable';
-// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
+import { SchemaColumnHeader } from './SchemaColumnHeader';
+import { SchemaRowBox } from './SchemaRowBox';
export enum ColumnType {
- Any,
Number,
String,
Boolean,
Doc,
Image,
- List,
- Date,
}
-// this map should be used for keys that should have a const type of value
-const columnTypes: Map<string, ColumnType> = new Map([
- ['title', ColumnType.String],
- ['x', ColumnType.Number],
- ['y', ColumnType.Number],
- ['_width', ColumnType.Number],
- ['_height', ColumnType.Number],
- ['_nativeWidth', ColumnType.Number],
- ['_nativeHeight', ColumnType.Number],
- ['isPrototype', ColumnType.Boolean],
- ['_curPage', ColumnType.Number],
- ['_currentTimecode', ColumnType.Number],
- ['zIndex', ColumnType.Number],
-]);
+
+const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate', 'text'];
@observer
export class CollectionSchemaView extends CollectionSubView() {
- private _previewCont?: HTMLDivElement;
-
- @observable _previewDoc: Doc | undefined = undefined;
- @observable _focusedTable: Doc = this.props.Document;
- @observable _col: any = '';
- @observable _menuWidth = 0;
- @observable _headerOpen = false;
- @observable _headerIsEditing = false;
- @observable _menuHeight = 0;
- @observable _pointerX = 0;
- @observable _pointerY = 0;
- @observable _openTypes: boolean = false;
+ private _closestDropIndex: number = 0;
+ private _previewRef: HTMLDivElement | null = null;
+ private _makeNewColumn: boolean = false;
- @computed get previewWidth() {
- return () => NumCast(this.props.Document.schemaPreviewWidth);
- }
- @computed get previewHeight() {
- return () => this.props.PanelHeight() - 2 * this.borderWidth;
- }
- @computed get tableWidth() {
- return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
- }
- @computed get borderWidth() {
- return Number(COLLECTION_BORDER_WIDTH);
- }
- @computed get scale() {
- return this.props.ScreenToLocalTransform().Scale;
- }
- @computed get columns() {
- return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
- }
- set columns(columns: SchemaHeaderField[]) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns);
- }
+ public static _rowHeight: number = 40;
+ public static _minColWidth: number = 25;
+ public static _rowMenuWidth: number = 60;
+ public static _previewDividerWidth: number = 4;
- @computed get menuCoordinates() {
- let searchx = 0;
- let searchy = 0;
- if (this.props.Document._searchDoc) {
- const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0];
- if (el !== undefined) {
- const rect = el.getBoundingClientRect();
- searchx = rect.x;
- searchy = rect.y;
- }
- }
- const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx;
- const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy;
- return this.props.ScreenToLocalTransform().transformPoint(x, y);
+ @computed get _selectedDocs() {
+ return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.props.Document));
}
+ @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
+ @observable _colEles: HTMLDivElement[] = [];
+ @observable _displayColumnWidths: number[] | undefined;
+ @observable _columnMenuIndex: number | undefined;
+ @observable _menuOptions: string[] = [];
+ @observable _newFieldWarning: string = '';
+ @observable _makeNewField: boolean = false;
+ @observable _newFieldDefault: any = 0;
+ @observable _newFieldType: ColumnType = ColumnType.Number;
+ @observable _menuValue: string = '';
+ @observable _filterColumnIndex: number | undefined;
+ @observable _filterValue: string = '';
get documentKeys() {
const docs = this.childDocs;
@@ -115,532 +73,811 @@ export class CollectionSchemaView extends CollectionSubView() {
//TODO Types
untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)))));
- this.columns.forEach(key => (keys[key.heading] = true));
+ // this.columns.forEach(key => (keys[key.heading] = true));
return Array.from(Object.keys(keys));
}
- @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing);
+ @computed get previewWidth() {
+ return NumCast(this.layoutDoc.schemaPreviewWidth);
+ }
- @undoBatch
- setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => {
- this._openTypes = false;
- if (columnTypes.get(columnField.heading)) return;
-
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setType(NumCast(type));
- columns[index] = columnField;
- this.columns = columns;
+ @computed get tableWidth() {
+ return this.props.PanelWidth() - this.previewWidth - (this.previewWidth === 0 ? 0 : CollectionSchemaView._previewDividerWidth);
+ }
+
+ @computed get columnKeys() {
+ return Cast(this.layoutDoc.columnKeys, listSpec('string'), defaultColumnKeys);
+ }
+
+ @computed get storedColumnWidths() {
+ let widths = Cast(
+ this.layoutDoc.columnWidths,
+ listSpec('number'),
+ this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length)
+ );
+
+ const totalWidth = widths.reduce((sum, width) => sum + width, 0);
+ if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) {
+ widths = widths.map(w => {
+ const proportion = w / totalWidth;
+ return proportion * (this.tableWidth - CollectionSchemaView._rowMenuWidth);
+ });
}
- });
- @undoBatch
- setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setColor(color);
- columns[index] = columnField;
- this.columns = columns; // need to set the columns to trigger rerender
+ return widths;
+ }
+
+ @computed get displayColumnWidths() {
+ return this._displayColumnWidths ?? this.storedColumnWidths;
+ }
+
+ @computed get sortField() {
+ return StrCast(this.layoutDoc.sortField);
+ }
+
+ @computed get sortDesc() {
+ return BoolCast(this.layoutDoc.sortDesc);
+ }
+
+ rowIndex(doc: Doc) {
+ return this.childDocs.indexOf(doc);
+ }
+
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ document.addEventListener('keydown', this.onKeyDown);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.onKeyDown);
+ }
+
+ @action
+ onKeyDown = (e: KeyboardEvent) => {
+ if (this._selectedDocs.length > 0) {
+ switch (e.key) {
+ case 'ArrowDown':
+ {
+ const lastDoc = this._selectedDocs.lastElement();
+ const lastIndex = this.rowIndex(lastDoc);
+ const curDoc = this.childDocs[lastIndex];
+ if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
+ !e.shiftKey && this.clearSelection();
+ const newDoc = this.childDocs[lastIndex + 1];
+ if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+ else this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1);
+ }
+ e.stopPropagation();
+ }
+ break;
+ case 'ArrowUp':
+ {
+ const firstDoc = this._selectedDocs.lastElement();
+ const firstIndex = this.rowIndex(firstDoc);
+ const curDoc = this.childDocs[firstIndex];
+ if (firstIndex > 0 && firstIndex < this.childDocs.length) {
+ !e.shiftKey && this.clearSelection();
+ const newDoc = this.childDocs[firstIndex - 1];
+ if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+ else this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1);
+ }
+ e.stopPropagation();
+ }
+ break;
+ }
}
};
@undoBatch
@action
- setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
- const columns = this.columns;
- columns.forEach(col => col.setDesc(undefined));
-
- const index = columns.findIndex(c => c.heading === columnField.heading);
- const column = columns[index];
- column.setDesc(descending);
- columns[index] = column;
- this.columns = columns;
+ setSort = (field: string | undefined, desc: boolean = false) => {
+ this.layoutDoc.sortField = field;
+ this.layoutDoc.sortDesc = desc;
};
- renderTypes = (col: any) => {
- if (columnTypes.get(col.heading)) return null;
+ addRow = (doc: Doc | Doc[]) => {
+ const result: boolean = this.addDocument(doc);
+ this.setSort(this.sortField, this.sortDesc);
+ return result;
+ };
- const type = col.type;
+ @undoBatch
+ @action
+ changeColumnKey = (index: number, newKey: string, defaultVal?: any) => {
+ if (!this.documentKeys.includes(newKey)) {
+ this.addNewKey(newKey, defaultVal);
+ }
- const anyType = (
- <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}>
- <FontAwesomeIcon icon={'align-justify'} size="sm" />
- Any
- </div>
- );
+ let currKeys = [...this.columnKeys];
+ currKeys[index] = newKey;
+ this.layoutDoc.columnKeys = new List<string>(currKeys);
+ };
- const numType = (
- <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}>
- <FontAwesomeIcon icon={'hashtag'} size="sm" />
- Number
- </div>
- );
+ @undoBatch
+ @action
+ addColumn = (key: string, defaultVal?: any) => {
+ if (!this.documentKeys.includes(key)) {
+ this.addNewKey(key, defaultVal);
+ }
- const textType = (
- <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}>
- <FontAwesomeIcon icon={'font'} size="sm" />
- Text
- </div>
- );
+ const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1);
+ const currWidths = this.storedColumnWidths.slice();
+ currWidths.splice(0, 0, newColWidth);
+ const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
+ this.layoutDoc.columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));
- const boolType = (
- <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}>
- <FontAwesomeIcon icon={'check-square'} size="sm" />
- Checkbox
- </div>
- );
+ let currKeys = this.columnKeys.slice();
+ currKeys.splice(0, 0, key);
+ this.layoutDoc.columnKeys = new List<string>(currKeys);
+ };
- const listType = (
- <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}>
- <FontAwesomeIcon icon={'list-ul'} size="sm" />
- List
- </div>
- );
+ @action
+ addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
- const docType = (
- <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}>
- <FontAwesomeIcon icon={'file'} size="sm" />
- Document
- </div>
- );
+ @undoBatch
+ @action
+ removeColumn = (index: number) => {
+ if (this.columnKeys.length === 1) return;
+ const currWidths = this.storedColumnWidths.slice();
+ currWidths.splice(index, 1);
+ const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
+ this.layoutDoc.columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));
+
+ let currKeys = this.columnKeys.slice();
+ currKeys.splice(index, 1);
+ this.layoutDoc.columnKeys = new List<string>(currKeys);
+ };
- const imageType = (
- <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}>
- <FontAwesomeIcon icon={'image'} size="sm" />
- Image
- </div>
- );
+ @action
+ startResize = (e: any, index: number) => {
+ this._displayColumnWidths = this.storedColumnWidths;
+ setupMoveUpEvents(this, e, (e, delta) => this.resizeColumn(e, index), this.finishResize, emptyFunction);
+ };
- const dateType = (
- <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}>
- <FontAwesomeIcon icon={'calendar'} size="sm" />
- Date
- </div>
- );
+ @action
+ resizeColumn = (e: PointerEvent, index: number) => {
+ if (this._displayColumnWidths) {
+ let shrinking;
+ let growing;
- const allColumnTypes = (
- <div className="columnMenu-types">
- {anyType}
- {numType}
- {textType}
- {boolType}
- {listType}
- {docType}
- {imageType}
- {dateType}
- </div>
- );
+ let change = e.movementX;
+
+ if (index !== 0) {
+ growing = change < 0 ? index : index - 1;
+ shrinking = change < 0 ? index - 1 : index;
+ }
- const justColType =
- type === ColumnType.Any
- ? anyType
- : type === ColumnType.Number
- ? numType
- : type === ColumnType.String
- ? textType
- : type === ColumnType.Boolean
- ? boolType
- : type === ColumnType.List
- ? listType
- : type === ColumnType.Doc
- ? docType
- : type === ColumnType.Date
- ? dateType
- : imageType;
+ if (shrinking === undefined || growing === undefined) return true;
- return (
- <div className="collectionSchema-headerMenu-group" onClick={action(() => (this._openTypes = !this._openTypes))}>
- <div>
- <label style={{ cursor: 'pointer' }}>Column type:</label>
- <FontAwesomeIcon icon={'caret-down'} size="lg" style={{ float: 'right', transform: `rotate(${this._openTypes ? '180deg' : 0})`, transition: '0.2s all ease' }} />
- </div>
- {this._openTypes ? allColumnTypes : justColType}
- </div>
- );
+ change = Math.abs(change);
+ if (this._displayColumnWidths[shrinking] - change < CollectionSchemaView._minColWidth) {
+ change = this._displayColumnWidths[shrinking] - CollectionSchemaView._minColWidth;
+ }
+
+ this._displayColumnWidths[shrinking] -= change * this.props.ScreenToLocalTransform().Scale;
+ this._displayColumnWidths[growing] += change * this.props.ScreenToLocalTransform().Scale;
+
+ return false;
+ }
+ return true;
};
- renderSorting = (col: any) => {
- const sort = col.desc;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Sort by:</label>
- <div className="columnMenu-sort">
- <div className={'columnMenu-option' + (sort === true ? ' active' : '')} onClick={() => this.setColumnSort(col, true)}>
- <FontAwesomeIcon icon="sort-amount-down" size="sm" />
- Sort descending
- </div>
- <div className={'columnMenu-option' + (sort === false ? ' active' : '')} onClick={() => this.setColumnSort(col, false)}>
- <FontAwesomeIcon icon="sort-amount-up" size="sm" />
- Sort ascending
- </div>
- <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}>
- <FontAwesomeIcon icon="times" size="sm" />
- Clear sorting
- </div>
- </div>
- </div>
- );
+ @action
+ finishResize = () => {
+ this.layoutDoc.columnWidths = new List<number>(this._displayColumnWidths);
+ this._displayColumnWidths = undefined;
};
- renderColors = (col: any) => {
- const selected = col.color;
+ @undoBatch
+ @action
+ swapColumns = (index1: number, index2: number) => {
+ const tempKey = this.columnKeys[index1];
+ const tempWidth = this.storedColumnWidths[index1];
+
+ let currKeys = this.columnKeys;
+ currKeys[index1] = currKeys[index2];
+ currKeys[index2] = tempKey;
+ this.layoutDoc.columnKeys = new List<string>(currKeys);
+
+ let currWidths = this.storedColumnWidths;
+ currWidths[index1] = currWidths[index2];
+ currWidths[index2] = tempWidth;
+ this.layoutDoc.columnWidths = new List<number>(currWidths);
+ };
- const pink = PastelSchemaPalette.get('pink2');
- const purple = PastelSchemaPalette.get('purple2');
- const blue = PastelSchemaPalette.get('bluegreen1');
- const yellow = PastelSchemaPalette.get('yellow4');
- const red = PastelSchemaPalette.get('red2');
- const gray = '#f1efeb';
+ @action
+ dragColumn = (e: PointerEvent, index: number) => {
+ const dragData = new DragManager.ColumnDragData(index);
+ const dragEles = [this._colEles[index]];
+ this.childDocs.forEach(doc => {
+ dragEles.push(this._rowEles.get(doc).children[1].children[index]);
+ });
+ DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y);
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Color:</label>
- <div className="columnMenu-colors">
- <div className={'columnMenu-colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div>
- </div>
- </div>
- );
+ return true;
};
- @undoBatch
@action
- changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, 'f1efeb')]);
+ addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);
+
+ @action
+ setColRef = (index: number, ref: HTMLDivElement) => {
+ if (this._colEles.length <= index) {
+ this._colEles.push(ref);
} else {
- if (addNew) {
- columns.push(new SchemaHeaderField(newKey, 'f1efeb'));
- this.columns = columns;
- } else {
- const index = columns.map(c => c.heading).indexOf(oldKey);
- if (index > -1) {
- const column = columns[index];
- column.setHeading(newKey);
- columns[index] = column;
- this.columns = columns;
- if (filter) {
- Doc.setDocFilter(this.props.Document, newKey, filter, 'match');
- } else {
- this.props.Document._docFilters = undefined;
- }
- }
- }
+ this._colEles[index] = ref;
}
};
@action
- openHeader = (col: any, screenx: number, screeny: number) => {
- this._col = col;
- this._headerOpen = true;
- this._pointerX = screenx;
- this._pointerY = screeny;
+ addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => {
+ const rowDocView = DocumentManager.Instance.getDocumentView(doc);
+ if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection);
};
@action
- closeHeader = () => {
- this._headerOpen = false;
+ clearSelection = () => SelectionManager.DeselectAll();
+
+ selectRows = (rootDoc: Doc, lastSelected: Doc) => {
+ const index = this.childDocs.indexOf(rootDoc);
+ const lastSelectedRow = this.childDocs.indexOf(lastSelected);
+ const startRow = Math.min(lastSelectedRow, index);
+ const endRow = Math.max(lastSelectedRow, index);
+ for (let i = startRow; i <= endRow; i++) {
+ const currDoc = this.childDocs[i];
+ if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true, i);
+ }
};
- @undoBatch
+ sortedSelectedDocs = () => this.childDocs.filter(doc => this._selectedDocs.includes(doc));
+
+ setDropIndex = (index: number) => (this._closestDropIndex = index);
+
@action
- deleteColumn = (key: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([]);
- } else {
- const index = columns.map(c => c.heading).indexOf(key);
- if (index > -1) {
- columns.splice(index, 1);
- this.columns = columns;
- }
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.columnDragData) {
+ e.stopPropagation();
+ const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0];
+ let i = de.complete.columnDragData.colIndex;
+ this.displayColumnWidths.reduce((total, curr, index) => {
+ if (total <= mouseX && total + curr >= mouseX) {
+ i = index;
+ }
+ return total + curr;
+ }, CollectionSchemaView._rowMenuWidth);
+ this.swapColumns(de.complete.columnDragData.colIndex, i);
+ e.stopPropagation();
+ return true;
}
- this.closeHeader();
+ const draggedDocs = de.complete.docDragData?.draggedDocuments;
+ if (draggedDocs && super.onInternalDrop(e, de)) {
+ const pushedDocs = this.childDocs.filter((doc, index) => index >= this._closestDropIndex && !draggedDocs.includes(doc));
+ const pushedAndDraggedDocs = [...pushedDocs, ...draggedDocs];
+ const removed = this.childDocs.slice().filter(doc => !pushedAndDraggedDocs.includes(doc));
+ this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
+ this.setSort(undefined);
+ SelectionManager.DeselectAll();
+ draggedDocs.forEach(doc => {
+ const draggedView = DocumentManager.Instance.getFirstDocumentView(doc);
+ if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
+ DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
+ });
+ e.stopPropagation();
+ return true;
+ }
+ return false;
};
- getPreviewTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth);
+ @action
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ super.onExternalDrop(e, {}, undoBatch(action(docus => docus.map((doc: Doc) => this.addDocument(doc)))));
+ this.setSort(undefined);
};
+ onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);
+
@action
- onHeaderClick = (e: React.PointerEvent) => {
- e.stopPropagation();
+ onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const nativeWidth = this._previewRef!.getBoundingClientRect();
+ const minWidth = 40;
+ const maxWidth = 1000;
+ const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
+ const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
+ this.layoutDoc.schemaPreviewWidth = width;
+ return false;
};
@action
- onWheel(e: React.WheelEvent) {
- const scale = this.props.ScreenToLocalTransform().Scale;
- this.props.isContentActive(true) && e.stopPropagation();
- }
+ addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
+ if (!value && !forceEmptyNote) return false;
+ const newDoc = Docs.Create.TextDocument(value, { title: value, _autoHeight: true });
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
+ return this.addRow(newDoc) || false;
+ };
- @computed get renderMenuContent() {
- TraceMobx();
- return (
- <div className="collectionSchema-header-menuOptions">
- {this.renderTypes(this._col)}
- {this.renderColors(this._col)}
- <div className="collectionSchema-headerMenu-group">
- <button
- onClick={() => {
- this.deleteColumn(this._col.heading);
- }}>
- Hide Column
- </button>
- </div>
- </div>
- );
- }
+ menuCallback = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
- private createTarget = (ele: HTMLDivElement) => {
- this._previewCont = ele;
- super.CreateDropTarget(ele);
+ DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+
+ ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
+ Doc.GetProto(this.props.Document)[name] = '';
+ this.addRow(Docs.Create.TextDocument('', { title: name, _autoHeight: true }));
+ });
+ ContextMenu.Instance.displayMenu(x, y, undefined, true);
};
- isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable;
+ focusDocument = (doc: Doc, options: DocFocusOptions) => {
+ Doc.BrushDoc(doc);
+
+ const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
+ if (found) {
+ const top = found.getBoundingClientRect().top;
+ const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
+ if (Math.floor(localTop[1]) !== 0) {
+ let focusSpeed = options.zoomTime ?? 500;
+ smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
+ return focusSpeed;
+ }
+ }
+ return undefined;
+ };
- @action setFocused = (doc: Doc) => (this._focusedTable = doc);
+ @computed get fieldDefaultInput() {
+ switch (this._newFieldType) {
+ case ColumnType.Number:
+ return <input type="number" name="" id="" value={this._newFieldDefault ?? 0} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
+ case ColumnType.Boolean:
+ return (
+ <>
+ <input type="checkbox" name="" id="" value={this._newFieldDefault} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} />
+ {this._newFieldDefault ? 'true' : 'false'}
+ </>
+ );
+ case ColumnType.String:
+ return <input type="text" name="" id="" value={this._newFieldDefault ?? ''} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
+ }
+ }
- @action setPreviewDoc = (doc: Opt<Doc>) => {
- SelectionManager.SelectSchemaViewDoc(doc);
- this._previewDoc = doc;
+ onSearchKeyDown = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'Enter':
+ this._menuOptions.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuOptions[0]) : action(() => (this._makeNewField = true))();
+ break;
+ case 'Escape':
+ this.closeColumnMenu();
+ break;
+ }
};
- //toggles preview side-panel of schema
@action
- toggleExpander = () => {
- this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
+ setKey = (key: string, defaultVal?: any) => {
+ if (this._makeNewColumn) {
+ this.addColumn(key, defaultVal);
+ } else {
+ this.changeColumnKey(this._columnMenuIndex!, key, defaultVal);
+ }
+ this.closeColumnMenu();
};
- onDividerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander);
+ setColumnValues = (key: string, value: string) => {
+ let success: boolean = true;
+ this.childDocs.forEach(doc => success && KeyValueBox.SetField(doc, key, value));
+ return success;
};
+
@action
- onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
- const nativeWidth = this._previewCont!.getBoundingClientRect();
- const minWidth = 40;
- const maxWidth = 1000;
- const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
- const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
- this.props.Document.schemaPreviewWidth = width;
- return false;
+ openColumnMenu = (index: number, newCol: boolean) => {
+ this._makeNewColumn = false;
+ this._columnMenuIndex = index;
+ this._menuValue = '';
+ this._menuOptions = this.documentKeys;
+ this._makeNewField = false;
+ this._newFieldWarning = '';
+ this._makeNewField = false;
+ this._makeNewColumn = newCol;
+ };
+
+ @action
+ closeColumnMenu = () => (this._columnMenuIndex = undefined);
+
+ @action
+ openFilterMenu = (index: number) => {
+ this._filterColumnIndex = index;
+ this._filterValue = this.getFieldFilters(this.columnKeys[this._filterColumnIndex!]).map(filter => filter.split(':')[1])[0];
};
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected(true)) e.stopPropagation();
- else this.props.select(false);
+ @action
+ closeFilterMenu = (setValue: boolean) => {
+ if (setValue) {
+ if (this._filterValue !== '') {
+ Doc.setDocFilter(this.Document, this.columnKeys[this._filterColumnIndex!], this._filterValue, 'check', false, undefined, false);
+ } else {
+ this.removeFieldFilters(this.columnKeys[this._filterColumnIndex!]);
+ }
}
+ this._filterColumnIndex = undefined;
};
- @computed
- get previewDocument(): Doc | undefined {
- return this._previewDoc;
- }
+ openContextMenu = (x: number, y: number, index: number) => {
+ this.closeColumnMenu();
+ this.closeFilterMenu(false);
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({
+ description: 'Change field',
+ event: () => this.openColumnMenu(index, false),
+ icon: 'pencil-alt',
+ });
+ ContextMenu.Instance.addItem({
+ description: 'Filter field',
+ event: () => this.openFilterMenu(index),
+ icon: 'filter',
+ });
+ ContextMenu.Instance.addItem({
+ description: 'Delete column',
+ event: () => this.removeColumn(index),
+ icon: 'trash',
+ });
+ ContextMenu.Instance.displayMenu(x, y, undefined, false);
+ };
- @computed
- get dividerDragger() {
- return this.previewWidth() === 0 ? null : (
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}>
- <div className="collectionSchemaView-dividerDragger" />
- </div>
- );
- }
+ @action
+ updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._menuValue = e.target.value;
+ this._menuOptions = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
+ };
+
+ getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
+
+ removeFieldFilters = (field: string) => {
+ this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
+ };
- @computed
- get previewPanel() {
+ onFilterKeyDown = (e: React.KeyboardEvent) => {
+ //prettier-ignore
+ switch (e.key) {
+ case 'Enter' : this.closeFilterMenu(true); break;
+ case 'Escape': this.closeFilterMenu(false);break;
+ }
+ };
+
+ @action
+ updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => (this._filterValue = e.target.value);
+
+ @computed get newFieldMenu() {
return (
- <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}>
- {!this.previewDocument ? null : (
- <DocumentView
- Document={this.previewDocument}
- DataDoc={undefined}
- fitContentsToBox={returnTrue}
- dontCenter={'y'}
- focus={DocUtils.DefaultFocus}
- renderDepth={this.props.renderDepth}
- rootSelected={this.rootSelected}
- PanelWidth={this.previewWidth}
- PanelHeight={this.previewHeight}
- isContentActive={returnTrue}
- isDocumentActive={returnFalse}
- ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
- searchFilterDocs={this.searchFilterDocs}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
+ <div className="schema-new-key-options">
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.Number}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.Number;
+ this._newFieldDefault = 0;
+ })}
/>
- )}
+ number
+ </div>
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.Boolean}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.Boolean;
+ this._newFieldDefault = false;
+ })}
+ />
+ boolean
+ </div>
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.String}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.String;
+ this._newFieldDefault = '';
+ })}
+ />
+ string
+ </div>
+ <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div>
+ <div className="schema-key-warning">{this._newFieldWarning}</div>
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ if (this.documentKeys.includes(this._menuValue)) {
+ this._newFieldWarning = 'Field already exists';
+ } else if (this._menuValue.length === 0) {
+ this._newFieldWarning = 'Field cannot be an empty string';
+ } else {
+ this.setKey(this._menuValue, this._newFieldDefault);
+ }
+ })}>
+ done
+ </div>
</div>
);
}
- @computed
- get schemaTable() {
+ @computed get keysDropdown() {
return (
- <SchemaTable
- Document={this.props.Document}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- childDocs={this.childDocs}
- CollectionView={this.props.CollectionView}
- ContainingCollectionView={this.props.ContainingCollectionView}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- fieldKey={this.props.fieldKey}
- renderDepth={this.props.renderDepth}
- moveDocument={this.props.moveDocument}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- active={this.props.isContentActive}
- onDrop={this.onExternalDrop}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- isSelected={this.props.isSelected}
- isFocused={this.isFocused}
- setFocused={this.setFocused}
- setPreviewDoc={this.setPreviewDoc}
- deleteDocument={this.props.removeDocument}
- addDocument={this.props.addDocument}
- dataDoc={this.props.DataDoc}
- columns={this.columns}
- documentKeys={this.documentKeys}
- headerIsEditing={this._headerIsEditing}
- openHeader={this.openHeader}
- onClick={this.onTableClick}
- onPointerDown={emptyFunction}
- onResizedChange={this.onResizedChange}
- setColumns={this.setColumns}
- reorderColumns={this.reorderColumns}
- changeColumns={this.changeColumns}
- setHeaderIsEditing={this.setHeaderIsEditing}
- changeColumnSort={this.setColumnSort}
- />
+ <div className="schema-key-search">
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this._makeNewField = true;
+ })}>
+ + new field
+ </div>
+ <div
+ className="schema-key-list"
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+ (e: WheelEvent) => {
+ if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ },
+ { passive: false }
+ )
+ }>
+ {this._menuOptions.map(key => (
+ <div
+ className="schema-key-search-result"
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.setKey(key);
+ }}>
+ {key}
+ </div>
+ ))}
+ </div>
+ </div>
);
}
- @computed
- public get schemaToolbar() {
+ @computed get renderColumnMenu() {
+ const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
return (
- <div className="collectionSchemaView-toolbar">
- <div className="collectionSchemaView-toolbar-item">
- <div id="preview-schema-checkbox-div">
- <input type="checkbox" key={'Show Preview'} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />
- Show Preview
- </div>
+ <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
+ <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+ {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this.closeColumnMenu();
+ })}>
+ cancel
</div>
</div>
);
}
- onSpecificMenu = (e: React.MouseEvent) => {
- if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) {
- const cm = ContextMenu.Instance;
- const options = cm.findByDescription('Options...');
- const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
- optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' });
- !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
- cm.displayMenu(e.clientX, e.clientY);
- (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this.
- e.stopPropagation();
- }
- };
+ @computed get renderFilterOptions() {
+ const keyOptions: string[] = [];
+ const columnKey = this.columnKeys[this._filterColumnIndex!];
+ this.childDocs.forEach(doc => {
+ const key = StrCast(doc[columnKey]);
+ if (keyOptions.includes(key) === false && (key.includes(this._filterValue) || !this._filterValue) && key !== '') {
+ keyOptions.push(key);
+ }
+ });
- @action
- onTableClick = (e: React.MouseEvent): void => {
- if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) {
- this.setPreviewDoc(undefined);
- } else {
- e.stopPropagation();
+ const filters = StrListCast(this.Document._docFilters);
+ for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
+ if (filters[i] === columnKey && keyOptions.includes(filters[i].split(':')[1]) === false) {
+ keyOptions.push(filters[i + 1]);
+ }
}
- this.setFocused(this.props.Document);
- this.closeHeader();
- };
- onResizedChange = (newResized: Resize[], event: any) => {
- const columns = this.columns;
- newResized.forEach(resized => {
- const index = columns.findIndex(c => c.heading === resized.id);
- const column = columns[index];
- column.setWidth(resized.value);
- columns[index] = column;
+ const options = keyOptions.map(key => {
+ let bool = false;
+ if (filters !== undefined) {
+ const ind = filters.findIndex(filter => filter.split(':')[1] === key);
+ const fields = ind === -1 ? undefined : filters[ind].split(':');
+ bool = fields ? fields[2] === 'check' : false;
+ }
+ return (
+ <div key={key} className="schema-filter-option">
+ <input
+ type="checkbox"
+ onPointerDown={e => e.stopPropagation()}
+ onClick={e => e.stopPropagation()}
+ onChange={action(e => {
+ if (e.target.checked) {
+ Doc.setDocFilter(this.props.Document, columnKey, key, 'check');
+ } else {
+ Doc.setDocFilter(this.props.Document, columnKey, key, 'remove');
+ }
+ })}
+ checked={bool}
+ />
+ <span style={{ paddingLeft: 4 }}>{key}</span>
+ </div>
+ );
});
- this.columns = columns;
- };
- @action
- setColumns = (columns: SchemaHeaderField[]) => (this.columns = columns);
-
- @undoBatch
- reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
- const columns = [...columnsValues];
- const oldIndex = columns.indexOf(toMove);
- const relIndex = columns.indexOf(relativeTo);
- const newIndex = oldIndex > relIndex && !before ? relIndex + 1 : oldIndex < relIndex && before ? relIndex - 1 : relIndex;
-
- if (oldIndex === newIndex) return;
-
- columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
- this.columns = columns;
- };
-
- onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation();
+ return options;
+ }
- render() {
- TraceMobx();
- if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0);
- const menuContent = this.renderMenuContent;
- const menu = (
- <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}>
- <Measure
- offset
- onResize={action((r: any) => {
- const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height);
- this._menuWidth = dim[0];
- this._menuHeight = dim[1];
+ @computed get renderFilterMenu() {
+ const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
+ return (
+ <div className="schema-filter-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
+ <input className="schema-filter-input" type="text" value={this._filterValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} />
+ {this.renderFilterOptions}
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this.closeFilterMenu(true);
})}>
- {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>}
- </Measure>
+ done
+ </div>
</div>
);
+ }
+
+ @computed get sortedDocs() {
+ const field = StrCast(this.layoutDoc.sortField);
+ const desc = BoolCast(this.layoutDoc.sortDesc);
+ const docs = !field
+ ? this.childDocs
+ : this.childDocs.sort((docA, docB) => {
+ const aStr = Field.toString(docA[field] as Field);
+ const bStr = Field.toString(docB[field] as Field);
+ var out = 0;
+ if (aStr < bStr) out = -1;
+ if (aStr > bStr) out = 1;
+ if (desc) out *= -1;
+ return out;
+ });
+ return { docs };
+ }
+ sortedDocsFunc = () => this.sortedDocs;
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0);
+ previewWidthFunc = () => this.previewWidth;
+ render() {
return (
- <div
- className={'collectionSchemaView' + (this.props.Document._searchDoc ? '-searchContainer' : '-container')}
- style={{
- overflow: this.props.scrollOverflow === true ? 'scroll' : undefined,
- backgroundColor: 'white',
- pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined,
- width: this.props.PanelWidth() || '100%',
- height: this.props.PanelHeight() || '100%',
- position: 'relative',
- }}>
+ <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
<div
- className="collectionSchemaView-tableContainer"
- style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
- onContextMenu={this.onSpecificMenu}
- onPointerDown={this.onPointerDown}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}
- onDrop={e => this.onExternalDrop(e, {})}
- ref={this.createTarget}>
- {this.schemaTable}
+ className="schema-table"
+ onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+ ref={r => {
+ // prevent wheel events from passively propagating up through containers
+ r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
+ }}>
+ <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}>
+ <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
+ <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
+ <FontAwesomeIcon icon="plus" />
+ </div>
+ </div>
+ {this.columnKeys.map((key, index) => (
+ <SchemaColumnHeader
+ key={index}
+ columnIndex={index}
+ columnKeys={this.columnKeys}
+ columnWidths={this.displayColumnWidths}
+ sortField={this.sortField}
+ sortDesc={this.sortDesc}
+ setSort={this.setSort}
+ removeColumn={this.removeColumn}
+ resizeColumn={this.startResize}
+ openContextMenu={this.openContextMenu}
+ dragColumn={this.dragColumn}
+ setColRef={this.setColRef}
+ />
+ ))}
+ </div>
+ {this._columnMenuIndex !== undefined && this.renderColumnMenu}
+ {this._filterColumnIndex !== undefined && this.renderFilterMenu}
+ <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} />
+
+ <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
</div>
- {this.dividerDragger}
- {!this.previewWidth() ? null : this.previewPanel}
- {this._headerOpen && this.props.isContentActive() ? menu : null}
+ {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
+ {this.previewWidth > 0 && (
+ <div style={{ width: `${this.previewWidth}px` }} ref={ref => (this._previewRef = ref)}>
+ {Array.from(this._selectedDocs).lastElement() && (
+ <DocumentView
+ Document={Array.from(this._selectedDocs).lastElement()}
+ DataDoc={undefined}
+ fitContentsToBox={returnTrue}
+ dontCenter={'y'}
+ onClickScriptDisable="always"
+ focus={DocUtils.DefaultFocus}
+ renderDepth={this.props.renderDepth + 1}
+ rootSelected={this.rootSelected}
+ PanelWidth={this.previewWidthFunc}
+ PanelHeight={this.props.PanelHeight}
+ isContentActive={returnTrue}
+ isDocumentActive={returnFalse}
+ ScreenToLocalTransform={this.screenToLocal}
+ docFilters={this.childDocFilters}
+ docRangeFilters={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.addRow}
+ removeDocument={this.props.removeDocument}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ />
+ )}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+interface CollectionSchemaViewDocsProps {
+ schema: CollectionSchemaView;
+ childDocs: () => { docs: Doc[] };
+}
+
+@observer
+class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
+ tableWidthFunc = () => this.props.schema.tableWidth;
+ rowHeightFunc = () => CollectionSchemaView._rowHeight;
+ childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -CollectionSchemaView._rowHeight - index * this.rowHeightFunc()));
+ render() {
+ return (
+ <div className="schema-table-content">
+ {this.props.childDocs().docs.map((doc: Doc, index: number) => {
+ const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc;
+ return (
+ <div className="schema-row-wrapper" style={{ maxHeight: CollectionSchemaView._rowHeight }}>
+ <DocumentView
+ key={doc[Id]}
+ {...this.props.schema.props}
+ LayoutTemplate={this.props.schema.props.childLayoutTemplate}
+ LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)}
+ Document={doc}
+ DataDoc={dataDoc}
+ renderDepth={this.props.schema.props.renderDepth + 1}
+ ContainingCollectionView={this.props.schema.props.CollectionView}
+ ContainingCollectionDoc={this.props.schema.Document}
+ PanelWidth={this.tableWidthFunc}
+ PanelHeight={this.rowHeightFunc}
+ styleProvider={DefaultStyleProvider}
+ waitForDoubleClickToClick={returnNever}
+ defaultDoubleClick={returnDefault}
+ enableDragWhenActive={true}
+ onClickScriptDisable="always"
+ focus={this.props.schema.focusDocument}
+ docFilters={this.props.schema.childDocFilters}
+ docRangeFilters={this.props.schema.childDocRangeFilters}
+ searchFilterDocs={this.props.schema.searchFilterDocs}
+ rootSelected={this.props.schema.rootSelected}
+ ScreenToLocalTransform={this.childScreenToLocal(index)}
+ bringToFront={emptyFunction}
+ isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
+ isContentActive={emptyFunction}
+ whenChildContentsActiveChanged={active => this.props.schema.props.whenChildContentsActiveChanged(active)}
+ hideDecorations={true}
+ hideTitle={true}
+ hideDocumentButtonBar={true}
+ hideLinkAnchors={true}
+ fitWidth={returnTrue}
+ scriptContext={this}
+ />
+ </div>
+ );
+ })}
</div>
);
}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
new file mode 100644
index 000000000..d88d67c94
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -0,0 +1,62 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { Colors } from '../../global/globalEnums';
+import './CollectionSchemaView.scss';
+
+export interface SchemaColumnHeaderProps {
+ columnKeys: string[];
+ columnWidths: number[];
+ columnIndex: number;
+ sortField: string;
+ sortDesc: boolean;
+ setSort: (field: string, desc: boolean) => void;
+ removeColumn: (index: number) => void;
+ resizeColumn: (e: any, index: number) => void;
+ dragColumn: (e: any, index: number) => boolean;
+ openContextMenu: (x: number, y: number, index: number) => void;
+ setColRef: (index: number, ref: HTMLDivElement) => void;
+}
+
+@observer
+export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> {
+ @computed get fieldKey() {
+ return this.props.columnKeys[this.props.columnIndex];
+ }
+
+ @action
+ sortClicked = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this.props.sortField == this.fieldKey) {
+ this.props.setSort(this.fieldKey, !this.props.sortDesc);
+ } else {
+ this.props.setSort(this.fieldKey, false);
+ }
+ };
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
+ };
+
+ render() {
+ return (
+ <div className="schema-column-header" style={{ width: this.props.columnWidths[this.props.columnIndex] }} onPointerDown={this.onPointerDown} ref={col => col && this.props.setColRef(this.props.columnIndex, col)}>
+ <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex)}></div>
+ <div className="schema-column-title">{this.fieldKey}</div>
+
+ <div className="schema-header-menu">
+ <div className="schema-header-button" onPointerDown={e => this.props.openContextMenu(e.clientX, e.clientY, this.props.columnIndex)}>
+ <FontAwesomeIcon icon="ellipsis-h" />
+ </div>
+ <div className="schema-sort-button" onPointerDown={this.sortClicked} style={this.props.sortField == this.fieldKey ? { backgroundColor: Colors.MEDIUM_BLUE } : {}}>
+ <FontAwesomeIcon icon="caret-right" style={this.props.sortField == this.fieldKey ? { transform: `rotate(${this.props.sortDesc ? '270deg' : '90deg'})` } : {}} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
new file mode 100644
index 000000000..2defaae00
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -0,0 +1,136 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { OpenWhere } from '../../nodes/DocumentView';
+import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { CollectionSchemaView } from './CollectionSchemaView';
+import './CollectionSchemaView.scss';
+import { SchemaTableCell } from './SchemaTableCell';
+
+@observer
+export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(SchemaRowBox, fieldKey);
+ }
+
+ private _ref: HTMLDivElement | null = null;
+
+ bounds = () => this._ref?.getBoundingClientRect();
+
+ @computed get schemaView() {
+ const vpath = this.props.docViewPath();
+ return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as CollectionSchemaView) : undefined;
+ }
+
+ @computed get schemaDoc() {
+ return this.props.ContainingCollectionDoc!;
+ }
+
+ @computed get rowIndex() {
+ return this.schemaView?.rowIndex(this.rootDoc) ?? -1;
+ }
+
+ componentDidMount(): void {
+ this.props.setContentView?.(this);
+ }
+
+ select = (ctrlKey: boolean, shiftKey: boolean) => {
+ if (!this.schemaView) return;
+ const lastSelected = Array.from(this.schemaView._selectedDocs).lastElement();
+ if (shiftKey && lastSelected) this.schemaView.selectRows(this.rootDoc, lastSelected);
+ else {
+ this.props.select?.(ctrlKey);
+ }
+ };
+
+ onPointerEnter = (e: any) => {
+ if (!SnappingManager.GetIsDragging()) return;
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointermove', this.onPointerMove);
+ };
+
+ onPointerMove = (e: any) => {
+ if (!SnappingManager.GetIsDragging()) return;
+ const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.context === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false;
+
+ if (this._ref && dragIsRow) {
+ const rect = this._ref.getBoundingClientRect();
+ const y = e.clientY - rect.top; //y position within the element.
+ const height = this._ref.clientHeight;
+ const halfLine = height / 2;
+ if (y <= halfLine) {
+ this._ref.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ this._ref.style.borderBottom = '0px';
+ this.schemaView?.setDropIndex(this.rowIndex);
+ } else if (y > halfLine) {
+ this._ref.style.borderTop = '0px';
+ this._ref.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ this.schemaView?.setDropIndex(this.rowIndex + 1);
+ }
+ }
+ };
+
+ onPointerLeave = (e: any) => {
+ if (this._ref) {
+ this._ref.style.borderTop = '0px';
+ this._ref.style.borderBottom = '0px';
+ }
+ document.removeEventListener('pointermove', this.onPointerMove);
+ };
+
+ render() {
+ return (
+ <div
+ className="schema-row"
+ style={{ height: CollectionSchemaView._rowHeight, backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined, pointerEvents: this.schemaView?.props.isContentActive() ? 'all' : undefined }}
+ onPointerEnter={this.onPointerEnter}
+ onPointerLeave={this.onPointerLeave}
+ ref={(row: HTMLDivElement | null) => {
+ row && this.schemaView?.addRowRef?.(this.rootDoc, row);
+ this._ref = row;
+ }}>
+ <div
+ className="row-menu"
+ style={{
+ width: CollectionSchemaView._rowMenuWidth,
+ pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
+ }}>
+ <div
+ className="schema-row-button"
+ onPointerDown={undoBatch(e => {
+ e.stopPropagation();
+ this.props.removeDocument?.(this.rootDoc);
+ })}>
+ <FontAwesomeIcon icon="times" />
+ </div>
+ <div
+ className="schema-row-button"
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
+ }}>
+ <FontAwesomeIcon icon="external-link-alt" />
+ </div>
+ </div>
+ <div className="row-cells">
+ {this.schemaView?.columnKeys?.map((key, index) => (
+ <SchemaTableCell
+ key={key}
+ Document={this.rootDoc}
+ fieldKey={key}
+ columnWidth={this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth}
+ isRowActive={this.props.isContentActive}
+ setColumnValues={(field, value) => this.schemaView?.setColumnValues(field, value) ?? false}
+ />
+ ))}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
deleted file mode 100644
index 16910cc83..000000000
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ /dev/null
@@ -1,694 +0,0 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from 'react-table';
-import { DateField } from '../../../../fields/DateField';
-import { AclPrivate, AclReadonly, DataSym, Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
-import { List } from '../../../../fields/List';
-import { listSpec } from '../../../../fields/Schema';
-import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
-import { GetEffectiveAcl } from '../../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../../Utils';
-import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { CompileScript, Transformer, ts } from '../../../util/Scripting';
-import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
-import '../../../views/DocumentDecorations.scss';
-import { ContextMenu } from '../../ContextMenu';
-import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
-import { PinProps } from '../../nodes/trails';
-import { DefaultStyleProvider } from '../../StyleProvider';
-import { CollectionView } from '../CollectionView';
-import {
- CellProps,
- CollectionSchemaButtons,
- CollectionSchemaCell,
- CollectionSchemaCheckboxCell,
- CollectionSchemaDateCell,
- CollectionSchemaDocCell,
- CollectionSchemaImageCell,
- CollectionSchemaListCell,
- CollectionSchemaNumberCell,
- CollectionSchemaStringCell,
-} from './CollectionSchemaCells';
-import { CollectionSchemaAddColumnHeader, KeysDropdown } from './CollectionSchemaHeaders';
-import { MovableColumn } from './CollectionSchemaMovableColumn';
-import { MovableRow } from './CollectionSchemaMovableRow';
-import './CollectionSchemaView.scss';
-
-enum ColumnType {
- Any,
- Number,
- String,
- Boolean,
- Doc,
- Image,
- List,
- Date,
-}
-
-// this map should be used for keys that should have a const type of value
-const columnTypes: Map<string, ColumnType> = new Map([
- ['title', ColumnType.String],
- ['x', ColumnType.Number],
- ['y', ColumnType.Number],
- ['_width', ColumnType.Number],
- ['_height', ColumnType.Number],
- ['_nativeWidth', ColumnType.Number],
- ['_nativeHeight', ColumnType.Number],
- ['isPrototype', ColumnType.Boolean],
- ['_curPage', ColumnType.Number],
- ['_currentTimecode', ColumnType.Number],
- ['zIndex', ColumnType.Number],
-]);
-
-export interface SchemaTableProps {
- Document: Doc; // child doc
- dataDoc?: Doc;
- PanelHeight: () => number;
- PanelWidth: () => number;
- childDocs?: Doc[];
- CollectionView: Opt<CollectionView>;
- ContainingCollectionView: Opt<CollectionView>;
- ContainingCollectionDoc: Opt<Doc>;
- fieldKey: string;
- renderDepth: number;
- deleteDocument?: (document: Doc | Doc[]) => boolean;
- addDocument?: (document: Doc | Doc[]) => boolean;
- moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- ScreenToLocalTransform: () => Transform;
- active: (outsideReaction: boolean | undefined) => boolean | undefined;
- onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
- addDocTab: (document: Doc, where: OpenWhere) => boolean;
- pinToPres: (document: Doc, pinProps: PinProps) => void;
- isSelected: (outsideReaction?: boolean) => boolean;
- isFocused: (document: Doc, outsideReaction: boolean) => boolean;
- setFocused: (document: Doc) => void;
- setPreviewDoc: (document: Opt<Doc>) => void;
- columns: SchemaHeaderField[];
- documentKeys: any[];
- headerIsEditing: boolean;
- openHeader: (column: any, screenx: number, screeny: number) => void;
- onClick: (e: React.MouseEvent) => void;
- onPointerDown: (e: React.PointerEvent) => void;
- onResizedChange: (newResized: Resize[], event: any) => void;
- setColumns: (columns: SchemaHeaderField[]) => void;
- reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void;
- changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void;
- setHeaderIsEditing: (isEditing: boolean) => void;
- changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void;
-}
-
-@observer
-export class SchemaTable extends React.Component<SchemaTableProps> {
- @observable _cellIsEditing: boolean = false;
- @observable _focusedCell: { row: number; col: number } = { row: 0, col: 0 };
- @observable _openCollections: Set<number> = new Set();
-
- @observable _showDoc: Doc | undefined;
- @observable _showDataDoc: any = '';
- @observable _showDocPos: number[] = [];
-
- @observable _showTitleDropdown: boolean = false;
-
- @computed get previewWidth() {
- return () => NumCast(this.props.Document.schemaPreviewWidth);
- }
- @computed get previewHeight() {
- return () => this.props.PanelHeight() - 2 * this.borderWidth;
- }
- @computed get tableWidth() {
- return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
- }
-
- @computed get childDocs() {
- if (this.props.childDocs) return this.props.childDocs;
-
- const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
- return DocListCast(doc[this.props.fieldKey]);
- }
- set childDocs(docs: Doc[]) {
- const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
- doc[this.props.fieldKey] = new List<Doc>(docs);
- }
-
- @computed get textWrappedRows() {
- return Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
- }
- set textWrappedRows(textWrappedRows: string[]) {
- this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows);
- }
-
- @computed get resized(): { id: string; value: number }[] {
- return this.props.columns.reduce((resized, shf) => {
- shf.width > -1 && resized.push({ id: shf.heading, value: shf.width });
- return resized;
- }, [] as { id: string; value: number }[]);
- }
- @computed get sorted(): SortingRule[] {
- return this.props.columns.reduce((sorted, shf) => {
- shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc });
- return sorted;
- }, [] as SortingRule[]);
- }
-
- @action
- changeSorting = (col: any) => {
- this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true);
- };
-
- @action
- changeTitleMode = () => (this._showTitleDropdown = !this._showTitleDropdown);
-
- @computed get borderWidth() {
- return Number(COLLECTION_BORDER_WIDTH);
- }
- @computed get tableColumns(): Column<Doc>[] {
- const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
- const columns: Column<Doc>[] = [];
- const tableIsFocused = this.props.isFocused(this.props.Document, false);
- const focusedRow = this._focusedCell.row;
- const focusedCol = this._focusedCell.col;
- const isEditable = !this.props.headerIsEditing;
-
- columns.push({
- expander: true,
- Header: '',
- width: 58,
- Expander: rowInfo => {
- return rowInfo.original.type !== DocumentType.COL ? null : (
- <div className="collectionSchemaView-expander" onClick={action(() => this._openCollections[rowInfo.isExpanded ? 'delete' : 'add'](rowInfo.viewIndex))}>
- <FontAwesomeIcon icon={rowInfo.isExpanded ? 'caret-down' : 'caret-right'} size="lg" />
- </div>
- );
- },
- });
- columns.push(
- ...this.props.columns.map(col => {
- const icon: IconProp =
- this.getColumnType(col) === ColumnType.Number
- ? 'hashtag'
- : this.getColumnType(col) === ColumnType.String
- ? 'font'
- : this.getColumnType(col) === ColumnType.Boolean
- ? 'check-square'
- : this.getColumnType(col) === ColumnType.Doc
- ? 'file'
- : this.getColumnType(col) === ColumnType.Image
- ? 'image'
- : this.getColumnType(col) === ColumnType.List
- ? 'list-ul'
- : this.getColumnType(col) === ColumnType.Date
- ? 'calendar'
- : 'align-justify';
-
- const keysDropdown = (
- <KeysDropdown
- keyValue={col.heading}
- possibleKeys={possibleKeys}
- existingKeys={this.props.columns.map(c => c.heading)}
- canAddNew={true}
- addNew={false}
- onSelect={this.props.changeColumns}
- setIsEditing={this.props.setHeaderIsEditing}
- docs={this.props.childDocs}
- Document={this.props.Document}
- dataDoc={this.props.dataDoc}
- fieldKey={this.props.fieldKey}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
- active={this.props.active}
- openHeader={this.props.openHeader}
- icon={icon}
- col={col}
- // try commenting this out
- width={'100%'}
- />
- );
-
- const sortIcon = col.desc === undefined ? 'caret-right' : col.desc === true ? 'caret-down' : 'caret-up';
- const header = (
- <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: '2px', display: 'flex', cursor: 'default', height: '100%' }}>
- {keysDropdown}
- <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: 'inline', zIndex: 1, background: 'inherit', cursor: 'pointer' }}>
- <FontAwesomeIcon icon={sortIcon} size="lg" />
- </div>
- {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */}
- </div>
- );
-
- return {
- Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />,
- accessor: (doc: Doc) => (doc ? Field.toString(doc[col.heading] as Field) : 0),
- id: col.heading,
- Cell: (rowProps: CellInfo) => {
- const rowIndex = rowProps.index;
- const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
- const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
-
- const props: CellProps = {
- row: rowIndex,
- col: columnIndex,
- rowProps: rowProps,
- isFocused: isFocused,
- changeFocusedCellByIndex: this.changeFocusedCellByIndex,
- CollectionView: this.props.CollectionView,
- ContainingCollection: this.props.ContainingCollectionView,
- Document: this.props.Document,
- fieldKey: this.props.fieldKey,
- renderDepth: this.props.renderDepth,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
- moveDocument: this.props.moveDocument,
- setIsEditing: this.setCellIsEditing,
- isEditable: isEditable,
- setPreviewDoc: this.props.setPreviewDoc,
- setComputed: this.setComputed,
- getField: this.getField,
- showDoc: this.showDoc,
- };
-
- switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) {
- case ColumnType.Number:
- return <CollectionSchemaNumberCell {...props} />;
- case ColumnType.String:
- return <CollectionSchemaStringCell {...props} />;
- case ColumnType.Boolean:
- return <CollectionSchemaCheckboxCell {...props} />;
- case ColumnType.Doc:
- return <CollectionSchemaDocCell {...props} />;
- case ColumnType.Image:
- return <CollectionSchemaImageCell {...props} />;
- case ColumnType.List:
- return <CollectionSchemaListCell {...props} />;
- case ColumnType.Date:
- return <CollectionSchemaDateCell {...props} />;
- default:
- return <CollectionSchemaCell {...props} />;
- }
- },
- minWidth: 200,
- };
- })
- );
- columns.push({
- Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />,
- accessor: (doc: Doc) => 0,
- id: 'add',
- Cell: (rowProps: CellInfo) => {
- const rowIndex = rowProps.index;
- const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
- const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
- return (
- <CollectionSchemaButtons
- {...{
- row: rowProps.index,
- col: columnIndex,
- rowProps: rowProps,
- isFocused: isFocused,
- changeFocusedCellByIndex: this.changeFocusedCellByIndex,
- CollectionView: this.props.CollectionView,
- ContainingCollection: this.props.ContainingCollectionView,
- Document: this.props.Document,
- fieldKey: this.props.fieldKey,
- renderDepth: this.props.renderDepth,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
- moveDocument: this.props.moveDocument,
- setIsEditing: this.setCellIsEditing,
- isEditable: isEditable,
- setPreviewDoc: this.props.setPreviewDoc,
- setComputed: this.setComputed,
- getField: this.getField,
- showDoc: this.showDoc,
- }}
- />
- );
- },
- width: 28,
- resizable: false,
- });
- return columns;
- }
-
- constructor(props: SchemaTableProps) {
- super(props);
- if (this.props.Document._schemaHeaders === undefined) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>([
- new SchemaHeaderField('title', '#f1efeb'),
- new SchemaHeaderField('author', '#f1efeb'),
- new SchemaHeaderField('*lastModified', '#f1efeb', ColumnType.Date),
- new SchemaHeaderField('text', '#f1efeb', ColumnType.String),
- new SchemaHeaderField('type', '#f1efeb'),
- new SchemaHeaderField('context', '#f1efeb', ColumnType.Doc),
- ]);
- }
- }
-
- componentDidMount() {
- document.addEventListener('keydown', this.onKeyDown);
- }
-
- componentWillUnmount() {
- document.removeEventListener('keydown', this.onKeyDown);
- }
-
- tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
- const tableDoc = this.props.Document[DataSym];
- const effectiveAcl = GetEffectiveAcl(tableDoc);
-
- if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) {
- doc.context = this.props.Document;
- tableDoc[this.props.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()));
- return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
- }
- return false;
- };
-
- private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
- return !rowInfo
- ? {}
- : {
- ScreenToLocalTransform: this.props.ScreenToLocalTransform,
- addDoc: this.tableAddDoc,
- removeDoc: this.props.deleteDocument,
- rowInfo,
- rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document, true),
- textWrapRow: this.toggleTextWrapRow,
- rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1,
- dropAction: StrCast(this.props.Document.childDropAction),
- addDocTab: this.props.addDocTab,
- };
- };
-
- private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
- if (!rowInfo || column) return {};
-
- const row = rowInfo.index;
- //@ts-ignore
- const col = this.columns.map(c => c.heading).indexOf(column!.id);
- const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true);
- // TODO: editing border doesn't work :(
- return {
- style: { border: !this.props.headerIsEditing && isFocused ? '2px solid rgb(255, 160, 160)' : '1px solid #f1efeb' },
- };
- };
-
- @action setCellIsEditing = (isEditing: boolean) => (this._cellIsEditing = isEditing);
-
- @action
- onKeyDown = (e: KeyboardEvent): void => {
- if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document, true)) {
- // && this.props.isSelected(true)) {
- const direction = e.key === 'Tab' ? 'tab' : e.which === 39 ? 'right' : e.which === 37 ? 'left' : e.which === 38 ? 'up' : e.which === 40 ? 'down' : '';
- this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
-
- if (direction) {
- const pdoc = FieldValue(this.childDocs[this._focusedCell.row]);
- pdoc && this.props.setPreviewDoc(pdoc);
- e.stopPropagation();
- }
- } else if (e.keyCode === 27) {
- this.props.setPreviewDoc(undefined);
- e.stopPropagation(); // stopPropagation for left/right arrows
- }
- };
-
- changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => {
- switch (direction) {
- case 'tab':
- return { row: curRow + 1 === this.childDocs.length ? 0 : curRow + 1, col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 };
- case 'right':
- return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 };
- case 'left':
- return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 };
- case 'up':
- return { row: curRow === 0 ? curRow : curRow - 1, col: curCol };
- case 'down':
- return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol };
- }
- return this._focusedCell;
- };
-
- @action
- changeFocusedCellByIndex = (row: number, col: number): void => {
- if (this._focusedCell.row !== row || this._focusedCell.col !== col) {
- this._focusedCell = { row: row, col: col };
- }
- this.props.setFocused(this.props.Document);
- };
-
- @undoBatch
- createRow = action(() => {
- this.props.addDocument?.(Docs.Create.TextDocument('', { title: '', _width: 100, _height: 30 }));
- this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col };
- });
-
- @undoBatch
- @action
- createColumn = () => {
- const newFieldName = (index: number) => `New field${index ? ` (${index})` : ''}`;
- for (let index = 0; index < 100; index++) {
- if (this.props.columns.findIndex(col => col.heading === newFieldName(index)) === -1) {
- this.props.columns.push(new SchemaHeaderField(newFieldName(index), '#f1efeb'));
- break;
- }
- }
- };
-
- @action
- getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => {
- if (doc && field && column.type === ColumnType.Any) {
- const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)];
- if (val instanceof ImageField) return ColumnType.Image;
- if (val instanceof Doc) return ColumnType.Doc;
- if (val instanceof DateField) return ColumnType.Date;
- if (val instanceof List) return ColumnType.List;
- }
- if (column.type && column.type !== 0) {
- return column.type;
- }
- if (columnTypes.get(column.heading)) {
- return (column.type = columnTypes.get(column.heading)!);
- }
- return (column.type = ColumnType.Any);
- };
-
- @undoBatch
- @action
- toggleTextwrap = async () => {
- const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
- if (textwrappedRows.length) {
- this.props.Document.textwrappedSchemaRows = new List<string>([]);
- } else {
- const docs = DocListCast(this.props.Document[this.props.fieldKey]);
- const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
- this.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- }
- };
-
- @action
- toggleTextWrapRow = (doc: Doc): void => {
- const textWrapped = this.textWrappedRows;
- const index = textWrapped.findIndex(id => doc[Id] === id);
-
- index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]);
-
- this.textWrappedRows = textWrapped;
- };
-
- @computed
- get reactTable() {
- const children = this.childDocs;
- const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false);
- const expanded: { [name: string]: any } = {};
- Array.from(this._openCollections.keys()).map(col => (expanded[col.toString()] = true));
- const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :((((
-
- return (
- <ReactTable
- style={{ position: 'relative' }}
- data={children}
- page={0}
- pageSize={children.length}
- showPagination={false}
- columns={this.tableColumns}
- getTrProps={this.getTrProps}
- getTdProps={this.getTdProps}
- sortable={false}
- TrComponent={MovableRow}
- sorted={this.sorted}
- expanded={expanded}
- resized={this.resized}
- onResizedChange={this.props.onResizedChange}
- // if it has a child, render another table with the children
- SubComponent={
- !hasCollectionChild
- ? undefined
- : row =>
- row.original.type !== DocumentType.COL ? null : (
- <div style={{ paddingLeft: 57 + 'px' }} className="reactTable-sub">
- <SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} />
- </div>
- )
- }
- />
- );
- }
-
- onContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: 'Toggle text wrapping', event: this.toggleTextwrap, icon: 'table' });
- };
-
- getField = (row: number, col?: number) => {
- const docs = this.childDocs;
-
- row = row % docs.length;
- while (row < 0) row += docs.length;
- const columns = this.props.columns;
- const doc = docs[row];
- if (col === undefined) {
- return doc;
- }
- if (col >= 0 && col < columns.length) {
- const column = this.props.columns[col].heading;
- return doc[column];
- }
- return undefined;
- };
-
- createTransformer = (row: number, col: number): Transformer => {
- const self = this;
- const captures: { [name: string]: Field } = {};
-
- const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
- return root => {
- function visit(node: ts.Node) {
- node = ts.visitEachChild(node, visit, context);
- if (ts.isIdentifier(node)) {
- const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
- const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
- if (isntPropAccess && isntPropAssign) {
- if (node.text === '$r') {
- return ts.createNumericLiteral(row.toString());
- } else if (node.text === '$c') {
- return ts.createNumericLiteral(col.toString());
- } else if (node.text === '$') {
- if (ts.isCallExpression(node.parent)) {
- // captures.doc = self.props.Document;
- // captures.key = self.props.fieldKey;
- }
- }
- }
- }
-
- return node;
- }
- return ts.visitNode(root, visit);
- };
- };
-
- // const getVars = () => {
- // return { capturedVariables: captures };
- // };
-
- return { transformer /*getVars*/ };
- };
-
- setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => {
- script = `const $ = (row:number, col?:number) => {
- const rval = (doc as any)[key][row + ${row}];
- return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading];
- }
- return ${script}`;
- const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) });
- if (compiled.compiled) {
- doc[field] = new ComputedField(compiled);
- return true;
- }
- return false;
- };
-
- @action
- showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => {
- this._showDoc = doc;
- if (dataDoc && screenX && screenY) {
- this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY);
- }
- };
-
- onOpenClick = () => {
- this._showDoc && this.props.addDocTab(this._showDoc, OpenWhere.addRight);
- };
-
- getPreviewTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(-this.borderWidth - 4 - this.tableWidth, -this.borderWidth);
- };
-
- render() {
- const preview = '';
- return (
- <div
- className="collectionSchemaView-table"
- onPointerDown={this.props.onPointerDown}
- onClick={this.props.onClick}
- onWheel={e => this.props.active(true) && e.stopPropagation()}
- onDrop={e => this.props.onDrop(e, {})}
- onContextMenu={this.onContextMenu}>
- {this.reactTable}
- {this.props.Document._chromeHidden || this.props.addDocument === returnFalse ? undefined : (
- <div className="collectionSchemaView-addRow" onClick={this.createRow}>
- + new
- </div>
- )}
- {!this._showDoc ? null : (
- <div
- className="collectionSchemaView-documentPreview"
- ref="overlay"
- style={{
- position: 'absolute',
- width: 150,
- height: 150,
- background: 'dimgray',
- display: 'block',
- top: 0,
- left: 0,
- transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)`,
- }}>
- <DocumentView
- Document={this._showDoc}
- DataDoc={this._showDataDoc}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- focus={DocUtils.DefaultFocus}
- renderDepth={this.props.renderDepth}
- rootSelected={returnFalse}
- isContentActive={returnTrue}
- isDocumentActive={returnFalse}
- PanelWidth={() => 150}
- PanelHeight={() => 150}
- ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- whenChildContentsActiveChanged={emptyFunction}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}></DocumentView>
- </div>
- )}
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
new file mode 100644
index 000000000..13e45963e
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -0,0 +1,69 @@
+import React = require('react');
+import { observer } from 'mobx-react';
+import { Doc, Field } from '../../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils';
+import { Transform } from '../../../util/Transform';
+import { EditableView } from '../../EditableView';
+import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { CollectionSchemaView } from './CollectionSchemaView';
+import './CollectionSchemaView.scss';
+
+export interface SchemaTableCellProps {
+ Document: Doc;
+ fieldKey: string;
+ columnWidth: number;
+ isRowActive: () => boolean | undefined;
+ setColumnValues: (field: string, value: string) => boolean;
+}
+
+@observer
+export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
+ render() {
+ const props: FieldViewProps = {
+ Document: this.props.Document,
+ docFilters: returnEmptyFilter,
+ docRangeFilters: returnEmptyFilter,
+ searchFilterDocs: returnEmptyDoclist,
+ styleProvider: DefaultStyleProvider,
+ docViewPath: returnEmptyDoclist,
+ ContainingCollectionView: undefined,
+ ContainingCollectionDoc: undefined,
+ fieldKey: this.props.fieldKey,
+ rootSelected: returnFalse,
+ isSelected: returnFalse,
+ setHeight: returnFalse,
+ select: emptyFunction,
+ dropAction: 'alias',
+ bringToFront: emptyFunction,
+ renderDepth: 1,
+ isContentActive: returnFalse,
+ whenChildContentsActiveChanged: emptyFunction,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ PanelWidth: () => this.props.columnWidth,
+ PanelHeight: () => CollectionSchemaView._rowHeight,
+ addDocTab: returnFalse,
+ pinToPres: returnZero,
+ };
+
+ return (
+ <div className="schema-table-cell" style={{ width: this.props.columnWidth }}>
+ <div className="schemacell-edit-wrapper" style={this.props.isRowActive() ? { cursor: 'text', pointerEvents: 'auto' } : { cursor: 'default', pointerEvents: 'none' }}>
+ <EditableView
+ contents={<FieldView {...props} />}
+ GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
+ SetValue={(value: string, shiftDown?: boolean, enterKey?: boolean) => {
+ if (shiftDown && enterKey) {
+ this.props.setColumnValues(this.props.fieldKey, value);
+ }
+ return KeyValueBox.SetField(this.props.Document, this.props.fieldKey, value);
+ }}
+ editing={this.props.isRowActive() ? undefined : false}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 430a36dce..3496bb835 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -59,7 +59,6 @@ $standard-border-radius: 3px;
// shadow
$standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
-$dashboardselector-height: 32px;
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
$docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
@@ -79,7 +78,7 @@ $TREE_BULLET_WIDTH: 20px;
MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
ANTIMODEMENU_HEIGHT: $antimodemenu-height;
- DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height;
+ TOPBAR_HEIGHT: $topbar-height;
DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 3375579d6..537ea1e5d 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -6,7 +6,7 @@ interface IGlobalScss {
MAX_ROW_HEIGHT: string;
SEARCH_THUMBNAIL_SIZE: string;
ANTIMODEMENU_HEIGHT: string;
- DASHBOARD_SELECTOR_HEIGHT: string;
+ TOPBAR_HEIGHT: string;
DFLT_IMAGE_NATIVE_DIM: string;
LEFT_MENU_WIDTH: string;
TREE_BULLET_WIDTH: string;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 4741fc6f2..29e7cd3ad 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -140,7 +140,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
: undefined;
if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true });
- LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false);
+ LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, false);
}
}
);
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 890ecc1b2..a6acf882c 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
@@ -445,7 +445,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
@@ -634,15 +634,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
step="0.1"
min="1"
max="5"
- value={this.timeline?._zoomFactor}
+ value={this.timeline?._zoomFactor ?? 1}
className="toolbar-slider"
id="zoom-slider"
- onPointerDown={(e: React.PointerEvent) => {
- e.stopPropagation();
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- this.zoom(Number(e.target.value));
- }}
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))}
/>
</div>
)}
@@ -658,7 +654,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return (
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
- {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
+ {...this.props}
+ CollectionFreeFormDocumentView={undefined}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.path}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 724394025..f99011b8f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -5,5 +5,5 @@
touch-action: manipulation;
top: 0;
left: 0;
- pointer-events: none;
-} \ No newline at end of file
+ //pointer-events: none;
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 901415d1e..9bdb2cee7 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -7,7 +7,6 @@ import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { numberRange } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index c229a966a..70ba7e182 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -51,7 +51,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]());
return (
<div
- className={`colorBox-container${this.isContentActive() ? '-interactive' : ''}`}
+ className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`}
onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
onClick={e => e.stopPropagation()}
style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}>
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ecffe6c4f..ace388c57 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -3,7 +3,7 @@ import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
@@ -122,7 +122,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
ref={r => {
//whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
}}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
isContentActive={returnFalse}
isDocumentActive={returnFalse}
styleProvider={this.docStyleProvider}
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
deleted file mode 100644
index df4c8f937..000000000
--- a/src/client/views/nodes/DataViz.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import './DataViz.scss';
-import { FieldView, FieldViewProps } from './FieldView';
-
-@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(DataVizBox, fieldKey);
- }
-
- render() {
- return (
- <div>
- <div>Hi</div>
- </div>
- );
- }
-}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..cd500e9ae 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -0,0 +1,4 @@
+.dataviz {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 592723ee9..eb25d3264 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,90 +1,151 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { StrCast } from "../../../../fields/Types";
-import { ViewBoxBaseComponent } from "../../DocComponent";
-import { FieldViewProps, FieldView } from "../FieldView";
-import "./DataVizBox.scss";
-import { HistogramBox } from "./HistogramBox";
-import { TableBox } from "./TableBox";
-
-enum DataVizView {
- TABLE = "table",
- HISTOGRAM= "histogram"
-}
+import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { CsvField } from '../../../../fields/URLField';
+import { Docs } from '../../../documents/Documents';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import { LineChart } from './components/LineChart';
+import { TableBox } from './components/TableBox';
+import './DataVizBox.scss';
+export enum DataVizView {
+ TABLE = 'table',
+ LINECHART = 'lineChart',
+}
@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
-
- // TODO: nda - make this use enum values instead
- // @observable private currView: DataVizView = DataVizView.TABLE;
- @computed get currView() {
- if (this.rootDoc._dataVizView) {
- return StrCast(this.rootDoc._dataVizView);
- } else {
- return "table";
- }
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DataVizBox, fieldKey);
}
-
- constructor(props: any) {
- super(props);
- if (!this.rootDoc._dataVizView) {
- // TODO: nda - this might not always want to default to "table"
- this.rootDoc._dataVizView = "table";
- }
+ // says we have an object and any string
+ // 2 ways of doing it
+ // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
+ // @observable private pairs: { [key: string]: FieldResult }[] = [];
+ static pairSet = new ObservableMap<string, { [key: string]: string }[]>();
+ @computed.struct get pairs() {
+ return DataVizBox.pairSet.get(StrCast(this.rootDoc.fileUpload));
}
+ private _chartRenderer: LineChart | undefined;
+ // // another way would be store a schema that defines the type of data we are expecting from an imported doc
+
+ // method1() {
+ // this.pairs[0].x = 3;
+ // }
+
+ // method() {
+ // // this.pairs[0].x = 3;
+ // // go through the pairs
+ // const x = this.pairs[0].x;
+ // if (typeof x == 'number') {
+ // let x1 = Number(x);
+ // // let x1 = NumCast(x);
+ // }
+ // }
+
+ // could use field result
+ // [key: string]: FieldResult;
+ // instead of numeric x,y in there,
+
+ // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+ @computed get dataVizView(): DataVizView {
+ return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
+ }
@action
- private createPairs() {
- const xVals: number[] = [0, 1, 2, 3, 4, 5];
- // const yVals: number[] = [10, 20, 30, 40, 50, 60];
- const yVals: number[] = [1, 2, 3, 4, 5, 6];
- let pairs: {
- x: number,
- y:number
- }[] = [];
- if (xVals.length != yVals.length) return pairs;
- for (let i = 0; i < xVals.length; i++) {
- pairs.push({x: xVals[i], y: yVals[i]});
+ restoreView = (data: Doc) => {
+ const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView);
+ const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._dataVizAxes = new List<string>(StrListCast(data.presDataVizAxes)));
+ const func = () => this._chartRenderer?.restoreView(data);
+ if (changedView || changedAxes) {
+ setTimeout(func, 100);
+ return true;
}
- this.pairs = pairs;
- return pairs;
+ return func() ?? false;
+ };
+
+ getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
+ const anchor =
+ this._chartRenderer?.getAnchor(pinProps) ??
+ Docs.Create.TextanchorDocument({
+ unrendered: true,
+ // when we clear selection -> we should have it so chartBox getAnchor returns undefined
+ // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
+ /*put in some options*/
+ });
+
+ anchor.presDataVizView = this.dataVizView;
+ anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
+
+ this.addDocument(anchor);
+ return anchor;
+ };
+
+ @computed.struct get axes() {
+ return StrListCast(this.layoutDoc.dataVizAxes);
}
+ selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List<string>(axes));
@computed get selectView() {
- switch(this.currView) {
- case "table":
- return (<TableBox pairs={this.pairs} />)
- case "histogram":
- return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ const width = this.props.PanelWidth() * 0.9;
+ const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
+ const margin = { top: 10, right: 25, bottom: 50, left:25};
+ if (!this.pairs) return 'no data';
+ // prettier-ignore
+ switch (this.dataVizView) {
+ case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>;
+ case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
}
}
-
- @computed get pairVals() {
- return this.createPairs();
+ @computed get dataUrl() {
+ return Cast(this.dataDoc[this.fieldKey], CsvField);
}
componentDidMount() {
- this.createPairs();
+ this.props.setContentView?.(this);
+ this.fetchData();
+ }
+
+ fetchData() {
+ if (DataVizBox.pairSet.has(StrCast(this.rootDoc.fileUpload))) return;
+ DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), []);
+ fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+ .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), res))));
}
// handle changing the view using a button
@action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
- this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table";
+ this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
}
render() {
- return (
- <div className="dataViz">
- <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ return !this.pairs?.length ? (
+ <div>Loading...</div>
+ ) : (
+ <div
+ className="dataViz"
+ onWheel={e => e.stopPropagation()}
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+ (e: WheelEvent) => {
+ if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ },
+ { passive: false }
+ )
+ }>
+ <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
{this.selectView}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts
deleted file mode 100644
index 595cecebf..000000000
--- a/src/client/views/nodes/DataVizBox/DrawHelper.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-export class PIXIPoint {
- public get x() { return this.coords[0]; }
- public get y() { return this.coords[1]; }
- public set x(value: number) { this.coords[0] = value; }
- public set y(value: number) { this.coords[1] = value; }
- public coords: number[] = [0, 0];
- constructor(x: number, y: number) {
- this.coords[0] = x;
- this.coords[1] = y;
- }
-}
-
-export class PIXIRectangle {
- public x: number;
- public y: number;
- public width: number;
- public height: number;
- public get left() { return this.x; }
- public get right() { return this.x + this.width; }
- public get top() { return this.y; }
- public get bottom() { return this.top + this.height; }
- public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
- constructor(x: number, y: number, width: number, height: number) {
- this.x = x;
- this.y = y;
- this.width = width;
- this.height = height;
- }
-}
-
-export class MathUtil {
-
- public static EPSILON: number = 0.001;
-
- public static Sign(value: number): number {
- return value >= 0 ? 1 : -1;
- }
-
- public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x += p2.x;
- p1.y += p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
- }
- }
-
- public static Perp(p1: PIXIPoint): PIXIPoint {
- return new PIXIPoint(-p1.y, p1.x);
- }
-
- public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x /= by;
- p1.y /= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x / by, p1.y / by);
- }
- }
-
- public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
- if (inline) {
- p1.x *= by;
- p1.y *= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x * by, p1.y * by);
- }
- }
-
- public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x -= p2.x;
- p1.y -= p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
- }
- }
-
- public static Area(rect: PIXIRectangle): number {
- return rect.width * rect.height;
- }
-
- public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
- // Return minimum distance between line segment vw and point p
- var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
- if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case
- // Consider the line extending the segment, parameterized as v + t (w - v).
- // We find projection of point p onto the line.
- // It falls where t = [(p-v) . (w-v)] / |w-v|^2
- // We clamp t from [0,1] to handle points outside the segment vw.
- var dot = MathUtil.Dot(
- MathUtil.SubtractPoint(p, v),
- MathUtil.SubtractPoint(w, v)) / l2;
- var t = Math.max(0, Math.min(1, dot));
- // Projection falls on the segment
- var projection = MathUtil.AddPoint(v,
- MathUtil.MultiplyConstant(
- MathUtil.SubtractPoint(w, v), t));
- return MathUtil.Dist(p, projection);
- }
-
- public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
- var a1 = pe1.y - ps1.y;
- var b1 = ps1.x - pe1.x;
-
- var a2 = pe2.y - ps2.y;
- var b2 = ps2.x - pe2.x;
-
- var delta = a1 * b2 - a2 * b1;
- if (delta === 0) {
- return undefined;
- }
- var c2 = a2 * ps2.x + b2 * ps2.y;
- var c1 = a1 * ps1.x + b1 * ps1.y;
- var invdelta = 1 / delta;
- return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
- }
-
- public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
- if (p.x < rect.left - this.EPSILON) {
- return false;
- }
- if (p.x > rect.right + this.EPSILON) {
- return false;
- }
- if (p.y < rect.top - this.EPSILON) {
- return false;
- }
- if (p.y > rect.bottom + this.EPSILON) {
- return false;
- }
-
- return true;
- }
-
- public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
- var r1 = new PIXIPoint(rect.left, rect.top);
- var r2 = new PIXIPoint(rect.right, rect.top);
- var r3 = new PIXIPoint(rect.right, rect.bottom);
- var r4 = new PIXIPoint(rect.left, rect.bottom);
- var ret = new Array<PIXIPoint>();
- var dist = this.Dist(lineFrom, lineTo);
- var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- return ret;
- }
-
- public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
- const left = Math.max(rect1.x, rect2.x);
- const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
- const top = Math.max(rect1.y, rect2.y);
- const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
- return new PIXIRectangle(left, top, right - left, bottom - top);
- }
-
- public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
- return Math.sqrt(MathUtil.DistSquared(p1, p2));
- }
-
- public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
- return p1.x * p2.x + p1.y * p2.y;
- }
-
- public static Normalize(p1: PIXIPoint) {
- var d = this.Length(p1);
- return new PIXIPoint(p1.x / d, p1.y / d);
- }
-
- public static Length(p1: PIXIPoint): number {
- return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
- }
-
- public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
- const a = p1.x - p2.x;
- const b = p1.y - p2.y;
- return (a * a + b * b);
- }
-
- public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
- return !(r2.x > r1.x + r1.width ||
- r2.x + r2.width < r1.x ||
- r2.y > r1.y + r1.height ||
- r2.y + r2.height < r1.y);
- }
-
- public static ArgMin(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] < value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static ArgMax(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] > value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static Combinations<T>(chars: T[]) {
- let result = new Array<T>();
- let f = (prefix: any, chars: any) => {
- for (let i = 0; i < chars.length; i++) {
- result.push(prefix.concat(chars[i]));
- f(prefix.concat(chars[i]), chars.slice(i + 1));
- }
- };
- f([], chars);
- return result;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss
deleted file mode 100644
index 5aac9dc77..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// change the stroke color of line-svg class
-.svgLine {
- position: absolute;
- background: darkGray;
- stroke: #000;
- stroke-width: 1px;
- width:100%;
- height:100%;
- opacity: 0.4;
-}
-
-.svgContainer {
- position: absolute;
- top:0;
- left:0;
- width:100%;
- height: 100%;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
deleted file mode 100644
index 00dc2ef46..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc } from "../../../../fields/Doc";
-import { NumCast } from "../../../../fields/Types";
-import "./HistogramBox.scss";
-
-interface HistogramBoxProps {
- rootDoc: Doc;
- pairs: {
- x: number,
- y: number
- }[]
-}
-
-
-export class HistogramBox extends React.Component<HistogramBoxProps> {
-
- private origin = {x: 0.1 * this.width, y: 0.9 * this.height};
-
- @computed get width() {
- return NumCast(this.props.rootDoc.width);
- }
-
- @computed get height() {
- return NumCast(this.props.rootDoc.height);
- }
-
- @computed get x() {
- return NumCast(this.props.rootDoc.x);
- }
-
- @computed get y() {
- return NumCast(this.props.rootDoc.y);
- }
-
- @computed get generatePoints() {
- // evenly distribute points along the x axis
- const xVals: number[] = this.props.pairs.map(p => p.x);
- const yVals: number[] = this.props.pairs.map(p => p.y);
-
- const xMin = Math.min(...xVals);
- const xMax = Math.max(...xVals);
- const yMin = Math.min(...yVals);
- const yMax = Math.max(...yVals);
-
- const xRange = xMax - xMin;
- const yRange = yMax - yMin;
-
- const xScale = this.width / xRange;
- const yScale = this.height / yRange;
-
- const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale;
- const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale;
-
- const points: {
- x: number,
- y: number
- }[] = this.props.pairs.map(p => {
- return {
- x: (p.x * xScale + xOffset) + this.origin.x,
- y: (p.y * yScale + yOffset)
- }
- });
-
- return points;
- }
-
- @computed get generateGraphLine() {
- const points = this.generatePoints;
- // loop through points and create a line from each point to the next
- let lines: {
- x1: number,
- y1: number,
- x2: number,
- y2: number
- }[] = [];
- for (let i = 0; i < points.length - 1; i++) {
- lines.push({
- x1: points[i].x,
- y1: points[i].y,
- x2: points[i + 1].x,
- y2: points[i + 1].y
- });
- }
- // generate array of svg with lines
- let svgLines: JSX.Element[] = [];
- for (let i = 0; i < lines.length; i++) {
- svgLines.push(
- <line
- className="svgLine"
- key={i}
- x1={lines[i].x1}
- y1={lines[i].y1}
- x2={lines[i].x2}
- y2={lines[i].y2}
- stroke="black"
- strokeWidth={2}
- />
- );
- }
-
- let res = [];
- for (let i = 0; i < svgLines.length; i++) {
- res.push(<svg className="svgContainer">{svgLines[i]}</svg>)
- }
- return res;
- }
-
- @computed get generateAxes() {
-
- const xAxis = {
- x1: 0.1 * this.width,
- x2: 0.9 * this.width,
- y1: 0.9 * this.height,
- y2: 0.9 * this.height,
- };
-
- const yAxis = {
- x1: 0.1 * this.width,
- x2: 0.1 * this.width,
- y1: 0.25 * this.height,
- y2: 0.9 * this.height,
- };
-
-
- return (
- [
- (<svg className="svgContainer">
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */}
- <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/>
-
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>),
- (
- <svg className="svgContainer">
- <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} />
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>)
- ]
- )
- }
-
-
- render() {
- return (
- <div>histogram box
- {/* <svg className="svgContainer">
- {this.generateSVGLine}
- </svg> */}
- {this.generateAxes[0]}
- {this.generateAxes[1]}
- {this.generateGraphLine.map(line => line)}
- </div>
- )
-
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
deleted file mode 100644
index dfa8262d8..000000000
--- a/src/client/views/nodes/DataVizBox/TableBox.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-
-interface TableBoxProps {
- pairs: {x: number, y:number}[]
-}
-
-
-export class TableBox extends React.Component<TableBoxProps> {
-
-
-
- render() {
- return (
- <div className="table-container">
- <table className="table">
- <thead>
- <tr className="table-row">
- <th>x</th>
- <th>y</th>
- </tr>
- </thead>
- <tbody>
- {this.props.pairs.map(p => {
- return (<tr className="table-row">
- <td>{p.x}</td>
- <td>{p.y}</td>
- </tr>)
- })}
- </tbody>
- </table>
- </div>
- )
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
new file mode 100644
index 000000000..d4f7bfb32
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -0,0 +1,41 @@
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+
+ .tooltip {
+ // make the height width bigger
+ width: fit-content;
+ height: fit-content;
+ }
+
+ .hoverHighlight-selected,
+ .selected {
+ // change the color of the circle element to be red
+ fill: transparent;
+ outline: red solid 2px;
+ border-radius: 100%;
+ position: absolute;
+ transform-box: fill-box;
+ transform-origin: center;
+ }
+ .hoverHighlight {
+ fill: transparent;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .hoverHighlight-selected {
+ fill: transparent;
+ scale: 1;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .datapoint {
+ fill: black;
+ }
+ .brushed {
+ // change the color of the circle element to be red
+ fill: red;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
new file mode 100644
index 000000000..777bf2f66
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -0,0 +1,319 @@
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+// import d3
+import * as d3 from 'd3';
+import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { LinkManager } from '../../../../util/LinkManager';
+import { PinProps, PresBox } from '../../trails';
+import { DataVizBox } from '../DataVizBox';
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
+import './Chart.scss';
+
+export interface DataPoint {
+ x: number;
+ y: number;
+}
+interface SelectedDataPoint extends DataPoint {
+ elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
+}
+export interface LineChartProps {
+ rootDoc: Doc;
+ axes: string[];
+ pairs: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class LineChart extends React.Component<LineChartProps> {
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ @observable _currSelected: SelectedDataPoint | undefined = undefined;
+ // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+
+ @computed get _lineChartData() {
+ if (this.props.axes.length <= 1) return [];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))
+ .sort((a, b) => (a.x < b.x ? -1 : 1));
+ }
+ @computed get incomingLinks() {
+ return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ .filter(link => link.anchor1 !== this.props.rootDoc) // get links where this chart doc is the target of the link
+ .map(link => DocCast(link.anchor1)); // then return the source of the link
+ }
+ @computed get incomingSelected() {
+ return this.incomingLinks // all links that are pointing to this node
+ .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes
+ .filter(dvb => dvb)
+ .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor
+ .lastElement();
+ }
+ @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
+ return minMaxRange([this._lineChartData]);
+ }
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet) {
+ this.drawChart([dataSet], this.rangeVals, w, h);
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true);
+ this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.annos = reaction(
+ () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']),
+ annotations => {
+ // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
+ // could be blue colored to make it look like anchor
+ // this.drawAnnotations()
+ // loop through annotations and draw them
+ annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
+ // this.drawAnnotations(annotations.x, annotations.y);
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.highlights = reaction(
+ () => ({
+ selected: this._currSelected,
+ incomingSelected: this.incomingSelected,
+ }),
+ ({ selected, incomingSelected }) => {
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
+ incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that
+
+ clearAnnotations = () => {
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ element.classList.remove('brushed');
+ element.classList.remove('selected');
+ }
+ };
+ // gets called whenever the "data-annotations" fields gets updated
+ drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => {
+ // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
+ // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY
+ // if it exists, then highlight it
+ // if it doesn't exist, then remove the highlight
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ const x = element.getAttribute('data-x');
+ const y = element.getAttribute('data-y');
+ if (x === dataX.toString() && y === dataY.toString()) {
+ element.classList.add(selected ? 'selected' : 'brushed');
+ }
+ // TODO: nda - this remove highlight code should go where we remove the links
+ // } else {
+ // }
+ }
+ };
+
+ removeAnnotations(dataX: number, dataY: number) {
+ // loop through and remove any annotations that no longer exist
+ }
+
+ @action
+ restoreView = (data: Doc) => {
+ const coords = Cast(data.presDataVizSelection, listSpec('number'), null);
+ if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) {
+ this.setCurrSelected(coords[0], coords[1]);
+ return true;
+ }
+ if (this._currSelected) {
+ this.setCurrSelected();
+ return true;
+ }
+ return false;
+ };
+
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
+ anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
+ return anchor;
+ };
+
+ @computed get height() {
+ return this.props.height - this.props.margin.top - this.props.margin.bottom;
+ }
+
+ @computed get width() {
+ return this.props.width - this.props.margin.left - this.props.margin.right;
+ }
+
+ setupTooltip() {
+ return d3
+ .select(this._lineChartRef.current)
+ .append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0)
+ .style('background', '#fff')
+ .style('border', '1px solid #ccc')
+ .style('padding', '5px')
+ .style('position', 'absolute')
+ .style('font-size', '12px');
+ }
+
+ // TODO: nda - use this everyewhere we update currSelected?
+ @action
+ setCurrSelected(x?: number, y?: number) {
+ // TODO: nda - get rid of svg element in the list?
+ this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined;
+ this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true));
+ this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined));
+ }
+
+ drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
+ if (this._lineChartSvg) {
+ const circleClass = '.circle-' + idx;
+ this._lineChartSvg
+ .selectAll(circleClass)
+ .data(data)
+ .join('circle') // enter append
+ .attr('class', `${circleClass} datapoint`)
+ .attr('r', '3') // radius
+ .attr('cx', d => xScale(d.x))
+ .attr('cy', d => yScale(d.y))
+ .attr('data-x', d => d.x)
+ .attr('data-y', d => d.y);
+ }
+ }
+
+ // TODO: nda - can use d3.create() to create html element instead of appending
+ drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ // clearing tooltip and the current chart
+ d3.select(this._lineChartRef.current).select('svg').remove();
+ d3.select(this._lineChartRef.current).select('.tooltip').remove();
+
+ const { xMin, xMax, yMin, yMax } = rangeVals;
+ if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
+ return;
+ }
+
+ // creating the x and y scales
+ const xScale = scaleCreatorNumerical(xMin, xMax, 0, width);
+ const yScale = scaleCreatorNumerical(0, yMax,height, 0);
+
+ // adding svg
+ const margin = this.props.margin;
+ const svg = (this._lineChartSvg = d3
+ .select(this._lineChartRef.current)
+ .append('svg')
+ .attr('width', `${width +margin.left + margin.right}`)
+ .attr('height', `${height + margin.top + margin.bottom }`)
+ .append('g')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`));
+
+ // create x and y grids
+ xGrid(svg.append('g'), height, xScale);
+ yGrid(svg.append('g'), width, yScale);
+ xAxisCreator(svg.append('g'), height, xScale);
+ yAxisCreator(svg.append('g'), width, yScale);
+
+ // draw the plot line
+ const data = dataSet[0];
+ const lineGen = createLineGenerator(xScale, yScale);
+ drawLine(svg.append('path'), data, lineGen);
+
+ // draw the datapoint circle
+ this.drawDataPoints(data, 0, xScale, yScale);
+
+ const higlightFocusPt = svg.append('g').style('display', 'none');
+ higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
+ const tooltip = this.setupTooltip();
+ // add all the tooltipContent to the tooltip
+ const mousemove = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ if (!d0) return;
+
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ const onPointClick = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ // find .circle-d1 with data-x = d0.x and data-y = d0.y
+ const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
+ this.setCurrSelected(d0.x, d0.y);
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ svg.append('rect')
+ .attr('class', 'overlay')
+ .attr('width', width)
+ .attr('height', this.height + margin.top + margin.bottom)
+ .attr('fill', 'none')
+ .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
+ .style('opacity', 0)
+ .on('mouseover', () => higlightFocusPt.style('display', null))
+ .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
+ .on('mousemove', mousemove)
+ .on('click', onPointClick);
+ };
+
+ private updateTooltip(
+ higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+ xScale: d3.ScaleLinear<number, number, never>,
+ d0: DataPoint,
+ yScale: d3.ScaleLinear<number, number, never>,
+ tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+ ) {
+ higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'hoverHighlight-selected' : 'hoverHighlight');
+ tooltip.transition().duration(300).style('opacity', 0.9);
+ // TODO: nda - updating the inner html could be deadly cause injection attacks!
+ tooltip
+ .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip
+ .style('pointer-events', 'none')
+ .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`);
+ }
+
+ render() {
+ const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none';
+ return (
+ <div ref={this._lineChartRef} className="chart-container">
+ <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss
index 1264d6a46..1264d6a46 100644
--- a/src/client/views/nodes/DataVizBox/TableBox.scss
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.scss
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
new file mode 100644
index 000000000..0d69ac890
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -0,0 +1,105 @@
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AnimationSym, Doc } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
+import { DragManager } from '../../../../util/DragManager';
+import { DocumentView } from '../../DocumentView';
+import { DataVizView } from '../DataVizBox';
+
+interface TableBoxProps {
+ pairs: { [key: string]: any }[];
+ selectAxes: (axes: string[]) => void;
+ axes: string[];
+ docView?: () => DocumentView | undefined;
+}
+
+@observer
+export class TableBox extends React.Component<TableBoxProps> {
+ @computed get columns() {
+ return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : [];
+ }
+ render() {
+ return (
+ <div className="table-container">
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ {this.columns
+ .filter(col => !col.startsWith('select'))
+ .map(col => {
+ const header = React.createRef<HTMLElement>();
+ return (
+ <th
+ ref={header as any}
+ style={{
+ color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined,
+ fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal',
+ }}
+ onPointerDown={e => {
+ const downX = e.clientX;
+ const downY = e.clientY;
+ setupMoveUpEvents(
+ {},
+ e,
+ e => {
+ const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const alias = Doc.MakeAlias(this.props.docView?.()!.rootDoc!);
+ alias._dataVizView = DataVizView.LINECHART;
+ alias._dataVizAxes = new List<string>([col, col]);
+ alias.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!;
+ return alias;
+ };
+ if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+ DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.linkDocument.linkDisplay = true;
+ // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ });
+ return true;
+ }
+ return false;
+ },
+ emptyFunction,
+ action(e => {
+ const newAxes = this.props.axes;
+ if (newAxes.includes(col)) {
+ newAxes.splice(newAxes.indexOf(col), 1);
+ } else if (newAxes.length >= 1) {
+ newAxes[1] = col;
+ } else {
+ newAxes[0] = col;
+ }
+ this.props.selectAxes(newAxes);
+ })
+ );
+ }}>
+ {col}
+ </th>
+ );
+ })}
+ </tr>
+ </thead>
+ <tbody>
+ {this.props.pairs?.map((p, i) => {
+ return (
+ <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}>
+ {this.columns.map(col => (
+ <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td>
+ ))}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
new file mode 100644
index 000000000..e1ff6f8eb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -0,0 +1,67 @@
+import * as d3 from 'd3';
+import { DataPoint } from '../components/LineChart';
+
+// TODO: nda - implement function that can handle range for strings
+
+export const minMaxRange = (dataPts: DataPoint[][]) => {
+ // find the max and min of all the data points
+ const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y)));
+ const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y)));
+
+ const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x)));
+ const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x)));
+
+ return { xMin, xMax, yMin, yMax };
+};
+
+export const scaleCreatorCategorical = (labels: string[], range: number[]) => {
+ const scale = d3.scaleBand().domain(labels).range(range);
+
+ return scale;
+};
+
+export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => {
+ return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]);
+};
+
+export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => {
+ // TODO: nda - look into the different types of curves
+ return d3
+ .line<DataPoint>()
+ .x(d => xScale(d.x))
+ .y(d => yScale(d.y))
+ .curve(d3.curveMonotoneX);
+};
+
+export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => {
+ console.log('x axis creator being called');
+ g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15));
+};
+
+export const yAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, marginLeft: number, yScale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'y-axis').call(d3.axisLeft(yScale));
+};
+
+export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'xGrid')
+ .attr('transform', `translate(0,${height})`)
+ .call(
+ d3
+ .axisBottom(scale)
+ .tickSize(-height)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, width: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'yGrid').call(
+ d3
+ .axisLeft(scale)
+ .tickSize(-width)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => {
+ p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen);
+};
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 2d1839dd8..dbcfe43cf 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed, trace } from 'mobx';
+import { computed } from 'mobx';
import { observer } from 'mobx-react';
import { AclPrivate, Doc, Opt } from '../../../fields/Doc';
import { ScriptField } from '../../../fields/ScriptField';
@@ -9,6 +9,7 @@ import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBo
import { CollectionDockingView } from '../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
+import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
import { CollectionView } from '../collections/CollectionView';
import { InkingStroke } from '../InkingStroke';
import { PresElementBox } from '../nodes/trails/PresElementBox';
@@ -31,6 +32,7 @@ import { KeyValueBox } from './KeyValueBox';
import { LabelBox } from './LabelBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { LinkBox } from './LinkBox';
+import { LoadingBox } from './LoadingBox';
import { MapBox } from './MapBox/MapBox';
import { PDFBox } from './PDFBox';
import { RecordingBox } from './RecordingBox';
@@ -42,7 +44,6 @@ import { VideoBox } from './VideoBox';
import { WebBox } from './WebBox';
import React = require('react');
import XRegExp = require('xregexp');
-import { LoadingBox } from './LoadingBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -126,9 +127,9 @@ export class DocumentContentsView extends React.Component<
TraceMobx();
if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
if (!this.layoutDoc) return '<p>awaiting layout</p>';
- if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString('data'));
+ if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString());
const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, 'layout')], 'string');
- if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(this.layoutDoc.proto ? 'proto' : '');
+ if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
if (typeof layout === 'string') return layout;
return '<p>Loading layout</p>';
}
@@ -138,7 +139,6 @@ export class DocumentContentsView extends React.Component<
return proto instanceof Promise ? undefined : proto;
}
get layoutDoc() {
- const params = StrCast(this.props.Document.PARAMS);
// bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script
// const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
const template: Doc =
@@ -146,7 +146,7 @@ export class DocumentContentsView extends React.Component<
(this.props.LayoutTemplateString && this.props.Document) ||
(this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) ||
Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
- return Doc.expandTemplateLayout(template, this.props.Document, params ? '(' + params + ')' : this.props.layoutKey);
+ return Doc.expandTemplateLayout(template, this.props.Document);
}
CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
@@ -266,12 +266,13 @@ export class DocumentContentsView extends React.Component<
HTMLtag,
ComparisonBox,
LoadingBox,
+ SchemaRowBox,
}}
bindings={bindings}
jsx={layoutFrame}
showWarnings={true}
onError={(test: any) => {
- console.log('DocumentContentsView:' + test);
+ console.log('DocumentContentsView:' + test, bindings, layoutFrame);
}}
/>
);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index a40599d85..df3299eef 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -71,11 +71,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
this.onLinkButtonMoved,
emptyFunction,
- action((e, doubleTap) => {
- if (doubleTap) {
- DocumentView.showBackLinks(this.props.View.rootDoc);
- }
- }),
+ action((e, doubleTap) => doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc)),
undefined,
undefined,
action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View))
@@ -146,7 +142,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
} else if (startLink !== endLink) {
endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
- const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
+ const linkDoc = DocUtils.MakeLink(startLink, endLink, { linkRelationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }, undefined, true);
LinkManager.currentLink = linkDoc;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index e5913d997..1265651ad 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -17,8 +17,7 @@
top: 0;
}
-.documentView-node,
-.documentView-node-topmost {
+.documentView-node {
position: inherit;
top: 0;
left: 0;
@@ -209,8 +208,7 @@
}
}
-.documentView-node:hover,
-.documentView-node-topmost:hover {
+.documentView-node:hover {
> .documentView-styleWrapper {
> .documentView-titleWrapper-hover {
display: inline-block;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b7a760c1e..f161a7b9b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,11 +1,9 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
-import { Document } from '../../../fields/documentSchemas';
+import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -13,9 +11,8 @@ import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
-import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
-import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnNone, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from '../../documents/Documents';
@@ -25,11 +22,10 @@ import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
-import { LinkFollower } from '../../util/LinkFollower';
+import { FollowLinkScript, LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
@@ -49,8 +45,6 @@ import './DocumentView.scss';
import { FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { LinkAnchorBox } from './LinkAnchorBox';
-import { RadialMenu } from './RadialMenu';
-import { ScriptingBox } from './ScriptingBox';
import { PresEffect, PresEffectDirection } from './trails';
import { PinProps, PresBox } from './trails/PresBox';
import React = require('react');
@@ -88,6 +82,7 @@ export enum OpenWhereMod {
right = 'right',
top = 'top',
bottom = 'bottom',
+ rightKeyValue = 'rightKeyValue',
}
export interface DocFocusOptions {
@@ -95,6 +90,7 @@ export interface DocFocusOptions {
willZoomCentered?: boolean; // determines whether to zoom in on target document
zoomScale?: number; // percent of containing frame to zoom into document
zoomTime?: number;
+ didMove?: boolean; // whether a document was changed during the showDocument process
docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
preview?: boolean; // whether changes should be previewed by the componentView or written to the document
@@ -112,14 +108,17 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p
export interface DocComponentView {
updateIcon?: () => void; // updates the icon representation of the document
getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+ restoreView?: (viewSpec: Doc) => boolean;
scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
+ select?: (ctrlKey: boolean, shiftKey: boolean) => void;
menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
isAnyChildContentActive?: () => boolean; // is any child content of the document active
+ onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
playFrom?: (time: number, endTime?: number) => void;
@@ -129,6 +128,8 @@ export interface DocComponentView {
setFocus?: () => void; // sets input focus to the componentView
componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
incrementalRendering?: () => void;
+ fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox)
+ overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document
fieldKey?: string;
annotationKey?: string;
getTitle?: () => string;
@@ -151,7 +152,6 @@ export interface DocumentViewSharedProps {
ContainingCollectionDoc: Opt<Doc>;
suppressSetHeight?: boolean;
thumbShown?: () => boolean;
- isHovering?: () => boolean;
setContentView?: (view: DocComponentView) => any;
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
PanelWidth: () => number;
@@ -161,6 +161,7 @@ export interface DocumentViewSharedProps {
childHideResizeHandles?: () => boolean;
dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
styleProvider: Opt<StyleProviderFunc>;
+ setTitleFocus?: () => void;
focus: DocFocusFunc;
fitWidth?: (doc: Doc) => boolean | undefined;
docFilters: () => string[];
@@ -187,6 +188,10 @@ export interface DocumentViewSharedProps {
ignoreAutoHeight?: boolean;
forceAutoHeight?: boolean;
disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
+ onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
+ enableDragWhenActive?: boolean;
+ waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
+ defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<string>;
scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
createNewFilterDoc?: () => void;
@@ -204,6 +209,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
hideDocumentButtonBar?: boolean;
hideOpenButton?: boolean;
hideDeleteButton?: boolean;
+ hideLinkAnchors?: boolean;
treeViewDoc?: Doc;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
@@ -230,8 +236,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
NativeWidth: () => number;
NativeHeight: () => number;
isSelected: (outsideReaction?: boolean) => boolean;
- isHovering: () => boolean;
- select: (ctrlPressed: boolean) => void;
+ select: (ctrlPressed: boolean, shiftPress?: boolean) => void;
DocumentView: () => DocumentView;
viewPath: () => DocumentView[];
}
@@ -239,19 +244,17 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
@observer
export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
- private _cursorTimer: NodeJS.Timeout | undefined;
- private _longPress = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _doubleClickTimeout: NodeJS.Timeout | undefined;
+ private _singleClickFunc: undefined | (() => any);
+ private _longPressSelector: NodeJS.Timeout | undefined;
private _downX: number = 0;
private _downY: number = 0;
private _downTime: number = 0;
- private _firstX: number = -1;
- private _firstY: number = -1;
private _lastTap: number = 0;
private _doubleTap = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
- private _timeout: NodeJS.Timeout | undefined;
private _dropDisposer?: DragManager.DragDropDisposer;
private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -259,12 +262,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
@observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset
@observable _animateScalingTo = 0;
- @observable _pendingDoubleClick = false;
- @observable _cursorPress = false;
- private get topMost() {
- return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
- }
public get animateScaleTime() {
return this._animateScaleTime ?? 300;
}
@@ -286,9 +284,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get thumb() {
return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
}
- @computed get hidden() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
- }
@computed get opacity() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity);
}
@@ -301,8 +296,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get widgetDecorations() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : ''));
}
- @computed get backgroundColor() {
- return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor);
+ @computed get backgroundBoxColor() {
+ const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb));
+ return thumb ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
}
@computed get docContents() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents);
@@ -310,10 +306,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get headerMargin() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
}
+ @computed get showCaption() {
+ return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0;
+ }
@computed get titleHeight() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
}
- @computed get pointerEvents() {
+ @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
}
@computed get finalLayoutKey() {
@@ -325,6 +324,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get nativeHeight() {
return this.props.NativeHeight();
}
+ @computed get disableClickScriptFunc() {
+ const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
+ // prettier-ignore
+ return (
+ DocumentView.LongPress ||
+ onScriptDisable === 'always' ||
+ (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
+ this._componentView?.isAnyChildContentActive?.()
+ );
+ }
@computed get onClickHandler() {
return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
}
@@ -344,7 +353,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentDidMount() {
this.setupHandlers();
}
- //componentDidUpdate() { this.setupHandlers(); }
+
setupHandlers() {
this.cleanupHandlers(false);
if (this._mainCont.current) {
@@ -362,316 +371,121 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
- this.removeMoveListeners();
- this.removeEndListeners();
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- if (RadialMenu.Instance._display === false) {
- this.addHoldMoveListeners();
- this.addHoldEndListeners();
- this.onRadialMenu(e, me);
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- this._firstX = pt.pageX;
- this._firstY = pt.pageY;
- }
- };
-
- handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-
- if (this._firstX === -1 || this._firstY === -1) {
- return;
- }
- if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
- this.handle1PointerHoldEnd(e, me);
- }
- };
-
- handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- this.removeHoldMoveListeners();
- this.removeHoldEndListeners();
- RadialMenu.Instance.closeMenu();
- this._firstX = -1;
- this._firstY = -1;
- SelectionManager.DeselectAll();
- me.touchEvent.stopPropagation();
- me.touchEvent.preventDefault();
- e.stopPropagation();
- if (RadialMenu.Instance.used) {
- this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
- }
- };
-
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (!this.props.isSelected()) {
- e.stopPropagation();
- e.preventDefault();
-
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- }
- };
-
- handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- SelectionManager.DeselectAll();
- if (this.Document.onPointerDown) return;
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch) {
- this._downX = touch.clientX;
- this._downY = touch.clientY;
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- e.stopPropagation();
- }
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
- }
- };
-
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (e.cancelBubble && this.props.isDocumentActive?.()) {
- this.removeMoveListeners();
- } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
- this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined);
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- };
-
- @action
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
- if (pinching !== 0 && oldPoint1 && oldPoint2) {
- const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX);
- const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY);
- const dX = -1 * Math.sign(dW);
- const dY = -1 * Math.sign(dH);
-
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(this.props.Document);
- const layoutDoc = Document(Doc.Layout(this.props.Document));
- let nwidth = Doc.NativeWidth(layoutDoc);
- let nheight = Doc.NativeHeight(layoutDoc);
- const width = layoutDoc._width || 0;
- const height = layoutDoc._height || (nheight / nwidth) * width;
- const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling;
- const actualdW = Math.max(width + dW * scale, 20);
- const actualdH = Math.max(height + dH * scale, 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (nwidth && nheight);
- if (fixedAspect && (!nwidth || !nheight)) {
- Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0));
- Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0));
- }
- if (nwidth > 0 && nheight > 0) {
- if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) {
- Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc));
- }
- layoutDoc._width = actualdW;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width;
- else layoutDoc._height = actualdH;
- } else {
- if (!fixedAspect) {
- Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc));
- }
- layoutDoc._height = actualdH;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height;
- else layoutDoc._width = actualdW;
- }
- } else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
- }
- }
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
- @action
- onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
-
- // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 });
- const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) &&
- RadialMenu.Instance.addItem({
- description: 'Delete',
- event: () => {
- this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu();
- },
- icon: 'external-link-square-alt',
- selected: -1,
- });
- // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 });
-
- SelectionManager.DeselectAll();
- };
-
startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
if (this._mainCont.current) {
- const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current);
+ const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()];
+ const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc));
const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0);
dragData.offset = this.props
.ScreenToLocalTransform()
.scale(this.NativeDimScaling)
.transformDirection(x - left, y - top);
- // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
- // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
dragData.dropAction = dropAction;
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;
dragData.canEmbed = this.props.canEmbedOnDrag;
- //dragData.dimSource :
- // dragEffects field, set dim
- // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
- // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
- // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
- DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
- setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
+ DragManager.StartDocumentDrag(
+ selected.map(dv => dv.docView!._mainCont.current!),
+ dragData,
+ x,
+ y,
+ { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) },
+ () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
); // this needs to happen after the drop event is processed.
ffview?.setupDragLines(false);
}
}
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- if (e.key === '†' || e.key === 't') {
- if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
- if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true));
- else if (!this._titleRef.current.setIsFocused(true)) {
- // if focus didn't change, focus on interior text...
- this._titleRef.current?.setIsFocused(false);
- this._componentView?.setFocus?.();
- }
- }
- }
- };
-
defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
const targetMatch =
Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
- (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties applie to this document
+ (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document
? true
: false;
return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
};
+ // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been)
+ setTitleFocus = () => {
+ if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
+ setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
+ };
+
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
- const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
(this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
- if (this._doubleTap && (![DocumentType.FONTICON, DocumentType.PRES].includes(this.props.Document.type as any) || this.onDoubleClickHandler)) {
- // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._pendingDoubleClick = false;
- this._timeout = undefined;
+ if (this._doubleTap) {
+ const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick;
+ if (this.onDoubleClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables
+ // prettier-ignore
+ const func = () => this.onDoubleClickHandler.script.run( {
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ thisContainer: this.props.ContainingCollectionDoc,
+ documentView: this.props.DocumentView(),
+ clientX, clientY, altKey, shiftKey, ctrlKey,
+ value: undefined,
+ }, console.log );
+ UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
+ } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) {
+ UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
+ } else {
+ this._singleClickFunc?.();
}
- if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey, ctrlKey } = e;
+ this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+ this._doubleClickTimeout = undefined;
+ this._singleClickFunc = undefined;
+ } else {
+ let clickFunc: undefined | (() => any);
+ if (!this.disableClickScriptFunc && this.onClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, metaKey } = e;
const func = () =>
- this.onDoubleClickHandler.script.run(
+ this.onClickHandler?.script.run(
{
this: this.layoutDoc,
self: this.rootDoc,
+ _readOnly_: false,
scriptContext: this.props.scriptContext,
thisContainer: this.props.ContainingCollectionDoc,
documentView: this.props.DocumentView(),
clientX,
clientY,
- altKey,
shiftKey,
- ctrlKey,
- value: undefined,
+ altKey,
+ metaKey,
},
console.log
- );
- UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
- } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
- //UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
- UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
- }
- } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey } = e;
- const func = () =>
- this.onClickHandler.script.run(
- {
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX,
- clientY,
- shiftKey,
- altKey,
- },
- console.log
- ).result?.select === true
- ? this.props.select(false)
- : '';
- const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
- if (this.onDoubleClickHandler && !this.props.Document.allowClickBeforeDoubleClick) {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(() => {
- this._timeout = undefined;
- clickFunc();
- }, 150);
- } else clickFunc();
- } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- SelectionManager.DeselectAll();
- this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
- } else {
- if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
+ ).result?.select === true
+ ? this.props.select(false)
+ : '';
+ clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
+ } else {
// onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
- stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
+ stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ }
+ preventDefault = false;
+ }
+
+ this._singleClickFunc = clickFunc ?? (() => (this._componentView?.select ?? this.props.select)(e.ctrlKey || e.metaKey, e.shiftKey));
+ const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
+ if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
+ this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+ this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
} else {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(
- action(() => {
- this._pendingDoubleClick = false;
- this._timeout = undefined;
- }),
- 350
- );
- this.props.select(e.ctrlKey || e.shiftKey);
+ this._singleClickFunc();
+ this._singleClickFunc = undefined;
}
- preventDefault = false;
}
stopPropagate && e.stopPropagation();
preventDefault && e.preventDefault();
@@ -680,70 +494,44 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView();
- if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
- // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
- if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
- if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- e.stopPropagation();
- if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
- // TODO: check here for panning/inking
- }
- return;
- }
- this._cursorTimer = setTimeout(
- action(() => {
- this._cursorPress = true;
- this.props.select(false);
- }),
- 1000 // long press required duration
- );
+ this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
+ if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
+
this._downX = e.clientX;
this._downY = e.clientY;
this._downTime = Date.now();
if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
- // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
+ // click events stop here if the document is active and no modes are overriding it
+ // if this is part of a template, let the event go up to the template root unless right/ctrl clicking
if (
- (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ // prettier-ignore
+ this.props.isDocumentActive?.() &&
!this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
- !e.ctrlKey &&
- (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
+ e.button === 0 &&
+ this.pointerEvents !== 'none' &&
!DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
) {
e.stopPropagation();
// don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though
//if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+
+ // listen to move events if document content isn't active or document is draggable
+ if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) {
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
}
- if (this.props.isDocumentActive?.()) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.addEventListener('pointermove', this.onPointerMove);
- }
- document.removeEventListener('pointerup', this.onPointerUp);
document.addEventListener('pointerup', this.onPointerUp);
- } else {
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
}
};
@action
onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) return;
- if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
-
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
+ if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+
+ if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
+ this.cleanupPointerEvents();
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
}
};
@@ -756,65 +544,38 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerUp = (e: PointerEvent): void => {
this.cleanupPointerEvents();
- this._longPress = this._cursorPress;
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
+ this._longPressSelector && clearTimeout(this._longPressSelector);
- const now = Date.now();
- if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ if (this.onPointerUpHandler?.script) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
- } else if (now - this._downTime < 300) {
- this._doubleTap = now - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
- // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
- if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
+ } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+ this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME;
+ if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
}
+ if (DocumentView.LongPress) e.preventDefault();
};
@undoBatch
@action
toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
- this.Document.ignoreClick = false;
- if (setTargetToggle) {
- this.Document.followLinkToggle = !this.Document.followLinkToggle;
- this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton;
- } else {
- this.Document._isLinkButton = !this.Document._isLinkButton;
- }
- if (this.Document._isLinkButton && !this.onClickHandler) {
- zoom !== undefined && (this.Document.followLinkZoom = zoom);
- } else if (this.Document._isLinkButton && this.onClickHandler) {
- this.Document._isLinkButton = false;
- this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
- };
- @undoBatch
- @action
- toggleTargetOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.followLinkToggle = true;
+ const hadOnClick = this.rootDoc.onClick;
+ this.noOnClick();
+ this.Document.onClick = hadOnClick ? undefined : FollowLinkScript();
+ this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never';
};
@undoBatch
@action
- followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
+ followLinkOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
+ this.Document.onClick = FollowLinkScript();
this.Document.followLinkToggle = false;
- this.Document.followLinkZoom = zoom;
- this.Document.followLinkLocation = location;
- };
- @undoBatch
- @action
- selectOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
- this.Document.followLinkToggle = false;
- this.Document.onClick = this.layoutDoc.onClick = undefined;
+ this.Document.followLinkZoom = false;
+ this.Document.followLinkLocation = undefined;
};
@undoBatch
noOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
+ this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined;
};
@undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
@@ -828,22 +589,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch
@action
- drop = async (e: Event, de: DragManager.DropEvent) => {
+ drop = (e: Event, de: DragManager.DropEvent) => {
if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
if (this.props.Document === Doc.ActiveDashboard) {
alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
return;
}
const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
- if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
- if (linkdrag?.linkSourceDoc) {
- e.stopPropagation();
- if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
- de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
- }
- if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
- const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document;
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
+ if (linkdrag) {
+ linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+ if (linkdrag.linkSourceDoc) {
+ e.stopPropagation();
+ if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+ de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+ }
+ if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
+ const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
+ de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, undefined, [de.x, de.y - 50]);
+ }
}
}
};
@@ -851,17 +614,34 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch
@action
makeIntoPortal = () => {
- const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
+ const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from');
if (!portalLink) {
DocUtils.MakeLink(
- { doc: this.props.Document },
- { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) },
- 'portal to:portal from'
+ this.props.Document,
+ Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }),
+ { linkRelationship: 'portal to:portal from' }
);
}
this.Document.followLinkLocation = OpenWhere.lightbox;
- this.Document.followLinkZoom = true;
- this.Document._isLinkButton = true;
+ this.Document.onClick = FollowLinkScript();
+ };
+
+ importDocument = () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.zip';
+ input.onchange = _e => {
+ if (input.files) {
+ const batch = UndoManager.StartBatch('importing');
+ Doc.importDocument(input.files[0]).then(doc => {
+ if (doc instanceof Doc) {
+ this.props.addDocTab(doc, OpenWhere.addRight);
+ batch.end();
+ }
+ });
+ }
+ };
+ input.click();
};
@action
@@ -915,25 +695,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const appearance = cm.findByDescription('UI Controls...');
const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
!Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
- !Doc.noviceMode &&
- appearanceItems.push({
- description: 'Add a Field',
- event: () => {
- const alias = Doc.MakeAlias(this.rootDoc);
- alias.layout = FormattedTextBox.LayoutString('newfield');
- alias.title = 'newfield';
- alias._height = 35;
- alias._width = 100;
- alias.syncLayoutFieldWithTitle = true;
- alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
- alias.y = NumCast(this.rootDoc.y);
- this.props.addDocument?.(alias);
- },
- icon: 'eye',
- });
- LinkManager.Links(this.Document).length &&
- appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
- !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
+ !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
const existingOnClick = cm.findByDescription('OnClick...');
@@ -952,29 +714,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
}
- !Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
+ onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
!Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
- this.props.CollectionFreeFormDocumentView &&
- onClicks.push({
- description: (this.Document.followLinkZoom ? "Don't" : '') + ' zoom following link',
- event: () => (this.Document.followLinkZoom = !this.Document.followLinkZoom),
- icon: this.Document.ignoreClick ? 'unlock' : 'lock',
- });
if (!this.Document.annotationOn) {
const options = cm.findByDescription('Options...');
const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
- onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' });
- onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
- onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(false, true), icon: 'map-pin' });
+ onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
!existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
} else if (LinkManager.Links(this.Document).length) {
- onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' });
- onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' });
- onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' });
+ onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' });
+ onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
!existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' });
}
}
@@ -990,7 +743,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const more = cm.findByDescription('More...');
const moreItems = more && 'subitems' in more ? more.subitems : [];
if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
if (!Doc.noviceMode) {
moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' });
moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' });
@@ -1002,18 +754,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
}
- moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
}
- if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
+ !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+ }
+ const constantItems: ContextMenuProps[] = [];
+
+ if (!Doc.IsSystem(this.rootDoc)) {
+ constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+ constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
+ (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+ if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
// need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
- moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
+ constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
}
- !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+ cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
}
const help = cm.findByDescription('Help...');
const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
- helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
+ helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
!Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
!Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
!Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
@@ -1052,7 +811,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
// Add link to help documentation
if (documentationDescription && documentationLink) {
- console.log('add documentation item');
helpItems.push({
description: documentationDescription,
event: () => {
@@ -1061,34 +819,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
icon: 'book',
});
}
- cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+ if (!help) cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+ else cm.moveAfter(help);
- if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
+ e?.stopPropagation(); // DocumentViews should stop propagation of this event
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
};
- collectionFilters = () => StrListCast(this.props.Document._docFilters);
- collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
- @computed get showFilterIcon() {
- return this.collectionFilters().length || this.collectionRangeDocFilters().length
- ? 'hasFilter'
- : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length
- ? 'inheritsFilter'
- : undefined;
- }
rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
- onClickFunc = () => this.onClickHandler;
+ onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
setHeight = (height: number) => (this.layoutDoc._height = height);
setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- isContentActive = (outsideReaction?: boolean) => {
+ isContentActive = (outsideReaction?: boolean): boolean | undefined => {
return this.props.isContentActive() === false
? false
: Doc.ActiveTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.rootSelected() ||
- this.props.Document.forceActive ||
+ this.rootDoc.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
this.props.isContentActive()
@@ -1102,7 +852,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
.concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
.some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
- return !this.props.isSelected() &&
+ return !this.props.LayoutTemplateString &&
+ !this.props.isSelected() &&
LightboxView.LightboxDoc !== this.rootDoc &&
this.thumb &&
!Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
@@ -1111,41 +862,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
? true
: false;
};
- get audioAnnoState() {
- return this.dataDoc.audioAnnoState ?? 'stopped';
- }
- @computed get audioAnnoView() {
- const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
- const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
- const audioIconColors = new Map<string, string>([
- ['recording', 'red'],
- ['playing', 'green'],
- ['stopped', audioAnnosCount ? 'blue' : 'gray'],
- ]);
- return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : (
- <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
- <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
- <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
- </div>
- </Tooltip>
- );
- }
+ contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
@computed get contents() {
TraceMobx();
return (
<div
className="documentView-contentsView"
style={{
- pointerEvents:
- this.opacity === 0
- ? 'none'
- : (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'
- ? 'none'
- : (this.props.contentPointerEvents as any)
- ? (this.props.contentPointerEvents as any)
- : this.rootDoc.type !== DocumentType.INK && this.isContentActive()
- ? 'all'
- : 'none',
+ pointerEvents: (this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents) ?? 'all',
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
{!this._retryThumb || !this.thumbShown() ? null : (
@@ -1156,20 +880,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
height={this.props.PanelHeight()}
onError={(e: any) => {
setTimeout(action(() => (this._retryThumb = 0)));
- setTimeout(
- action(() => (this._retryThumb = 1)),
- 150
- );
+ // prettier-ignore
+ setTimeout(action(() => (this._retryThumb = 1)), 150 );
}}
/>
)}
<DocumentContentsView
key={1}
{...this.props}
- pointerEvents={this.opacity === 0 ? returnNone : this.props.pointerEvents}
+ pointerEvents={this.contentPointerEvents}
docViewPath={this.props.viewPath}
thumbShown={this.thumbShown}
- isHovering={this.props.isHovering}
setContentView={this.setContentView}
NativeDimScaling={this.props.NativeDimScaling}
PanelHeight={this.panelHeight}
@@ -1178,23 +899,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
ScreenToLocalTransform={this.screenToLocal}
rootSelected={this.rootSelected}
onClick={this.onClickFunc}
- focus={emptyFunction}
+ focus={this.props.focus}
+ setTitleFocus={this.setTitleFocus}
layoutKey={this.finalLayoutKey}
/>
{this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
- {this.audioAnnoView}
</div>
);
}
- get indicatorIcon() {
- if (this.props.Document['acl-Public'] !== SharingPermissions.None) return 'globe-americas';
- else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return 'users';
- else return 'user';
- }
-
- @undoBatch
- hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true);
anchorPanelWidth = () => this.props.PanelWidth() || 1;
anchorPanelHeight = () => this.props.PanelHeight() || 1;
anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
@@ -1202,7 +915,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
switch (property.split(':')[0]) {
case StyleProp.ShowTitle: return '';
case StyleProp.PointerEvents: return 'none';
- case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox
case StyleProp.Highlighting: return undefined;
}
return this.props.styleProvider?.(doc, props, property);
@@ -1227,26 +939,28 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc);
}
+ hideLink = computedFn((link: Doc) => () => (link.linkDisplay = false));
@computed get allLinkEndpoints() {
// the small blue dots that mark the endpoints of links
TraceMobx();
- if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
- if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null;
+ if (this.props.hideLinkAnchors || this.layoutDoc.hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null;
const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay);
- return filtered.map((link, i) => (
+ return filtered.map(link => (
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView
{...this.props}
isContentActive={returnFalse}
Document={link}
+ docViewPath={this.props.viewPath}
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
dontRegisterView={false}
showTitle={returnEmptyString}
hideCaptions={true}
+ hideLinkAnchors={true}
fitWidth={returnTrue}
+ removeDocument={this.hideLink(link)}
styleProvider={this.anchorStyleProvider}
- removeDocument={this.hideLinkAnchor}
LayoutTemplate={undefined}
LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)}
/>
@@ -1254,26 +968,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
));
}
- @action
- playAnnotation = () => {
- const self = this;
- const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
- const anno = audioAnnos?.lastElement();
- if (anno instanceof AudioField && this.audioAnnoState === 'stopped') {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => (self.dataDoc.audioAnnoState = 'stopped'));
- },
- });
- this.dataDoc.audioAnnoState = 'playing';
- }
- };
-
static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
let gumStream: any;
let recorder: any;
@@ -1321,34 +1015,50 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
else setTimeout(stopFunc, 5000);
});
}
+ playAnnotation = () => {
+ const self = this;
+ const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
+ const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+ const anno = audioAnnos?.lastElement();
+ if (anno instanceof AudioField && audioAnnoState === 'stopped') {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
+ });
+ this.dataDoc.audioAnnoState = 'playing';
+ }
+ };
- captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+ captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
@computed get innards() {
TraceMobx();
const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
const showTitle = this.ShowTitle?.split(':')[0];
const showTitleHover = this.ShowTitle?.includes(':hover');
- const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
- const captionView = !showCaption ? null : (
+ const captionView = !this.showCaption ? null : (
<div
className="documentView-captionWrapper"
style={{
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
maxHeight: `max(100%, ${20 * ffscale()}px)`,
}}>
<FormattedTextBox
- {...OmitKeys(this.props, ['children']).omit}
+ {...this.props}
yPadding={10}
xPadding={10}
- fieldKey={showCaption}
+ fieldKey={this.showCaption}
fontSize={12 * Math.max(1, (2 * ffscale()) / 3)}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
noSidebar={true}
dontScale={true}
+ renderDepth={this.props.renderDepth}
isContentActive={this.isContentActive}
- onClick={this.onClickFunc}
/>
</div>
);
@@ -1367,7 +1077,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
color: lightOrDark(background),
background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
<EditableView
ref={this._titleRef}
@@ -1400,7 +1110,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
/>
</div>
);
- return this.props.hideTitle || (!showTitle && !showCaption) ? (
+ return this.props.hideTitle || (!showTitle && !this.showCaption) ? (
this.contents
) : (
<div className="documentView-styleWrapper">
@@ -1412,34 +1122,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
}
- @observable _: string = '';
+
renderDoc = (style: object) => {
TraceMobx();
- const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
- const isButton = this.props.Document.type === DocumentType.FONTICON;
- if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
- return (
- this.docContents ?? (
- <div
- className={`documentView-node${this.topMost ? '-topmost' : ''}`}
- id={this.props.Document[Id]}
- style={{
- ...style,
- background: isButton || thumb ? undefined : this.backgroundColor,
- opacity: this.opacity,
- cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
- color: StrCast(this.layoutDoc.color, 'inherit'),
- fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
- fontSize: Cast(this.Document._fontSize, 'string', null),
- transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
- }}>
- {this.innards}
- {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
- {this.widgetDecorations ?? null}
- </div>
- )
- );
+ return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate
+ ? null
+ : this.docContents ?? (
+ <div
+ className="documentView-node"
+ id={this.Document[Id]}
+ style={{
+ ...style,
+ background: this.backgroundBoxColor,
+ opacity: this.opacity,
+ cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
+ color: StrCast(this.layoutDoc.color, 'inherit'),
+ fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
+ fontSize: Cast(this.Document._fontSize, 'string', null),
+ transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
+ transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+ }}>
+ {this.innards}
+ {this.widgetDecorations ?? null}
+ </div>
+ );
};
/**
@@ -1474,7 +1180,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
render() {
TraceMobx();
const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
- const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
+ const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
const boxShadow =
this.props.treeViewDoc || !highlighting
? this.boxShadow
@@ -1486,50 +1192,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
+ clipPath: borderPath?.clipPath,
});
- const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
return (
<div
className={`${DocumentView.ROOT_DIV} docView-hack`}
ref={this._mainCont}
onContextMenu={this.onContextMenu}
- onKeyDown={this.onKeyDown}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)}
style={{
- display: this.hidden ? 'inline' : undefined,
borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents,
+ pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
}}>
- {!borderPath.path ? (
- animRenderDoc
- ) : (
- <>
- {animRenderDoc}
- <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
- <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
- <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
- </svg>
- </div>
- </>
- )}
- {this.showFilterIcon ? (
- <FontAwesomeIcon
- icon={'filter'}
- size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: this.showFilterIcon === 'hasFilter' ? '#18c718bd' : 'orange', zIndex: 1 }}
- onPointerDown={action(e => {
- this.props.select(false);
- SettingsManager.propertiesWidth = 250;
- e.stopPropagation();
- })}
- />
- ) : null}
+ <>
+ {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)}
+ {borderPath?.jsx}
+ </>
</div>
);
}
@@ -1538,6 +1221,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
public static ROOT_DIV = 'documentView-effectsWrapper';
+ @observable public static LongPress = false;
+ @observable public docView: DocumentViewInternal | undefined | null;
+ @observable public textHtmlOverlay: Opt<string>;
+ @observable private _isHovering = false;
+
+ public htmlOverlayEffect = '';
public get displayName() {
return 'DocumentView(' + this.props.Document?.title + ')';
} // this makes mobx trace() statements more descriptive
@@ -1549,6 +1238,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
this.ViewTimer && clearTimeout(this.ViewTimer);
this.rootDoc._viewTransition = undefined;
};
+ public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+
+ public showContextMenu = (pageX: number, pageY: number) => this.docView?.onContextMenu(undefined, pageX, pageY);
+
public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer);
this.rootDoc[AnimationSym] = presEffect;
@@ -1580,34 +1273,18 @@ export class DocumentView extends React.Component<DocumentViewProps> {
);
}
+ // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
public static showBackLinks(linkSource: Doc) {
- const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
- DocServer.GetRefField(docid).then(docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(linkSource);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- const linkCollection =
- (docx instanceof Doc && docx) ||
- Docs.Create.StackingDocument(
- [
- /*rootAlias()*/
- ],
- { title: linkSource.title + '-pivot', _width: 500, _height: 500 },
- docid
- );
- linkCollection.linkSource = linkSource;
- if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)');
- LightboxView.SetLightboxDoc(linkCollection);
- });
+ const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
+ // prettier-ignore
+ DocServer.GetRefField(docId).then(docx => docx instanceof Doc &&
+ LightboxView.SetLightboxDoc(
+ docx || // reuse existing pivot view of documents, or else create a new collection
+ Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId)
+ )
+ );
}
- @observable public docView: DocumentViewInternal | undefined | null;
-
- showContextMenu(pageX: number, pageY: number) {
- return this.docView?.onContextMenu(undefined, pageX, pageY);
- }
get Document() {
return this.props.Document;
}
@@ -1615,13 +1292,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.renderDepth === 0;
}
get rootDoc() {
- return this.docView?.rootDoc || this.Document;
+ return this.docView?.rootDoc ?? this.Document;
}
get dataDoc() {
- return this.docView?.dataDoc || this.Document;
- }
- get finalLayoutKey() {
- return this.docView?.finalLayoutKey || 'layout';
+ return this.docView?.dataDoc ?? this.Document;
}
get ContentDiv() {
return this.docView?.ContentDiv;
@@ -1635,20 +1309,22 @@ export class DocumentView extends React.Component<DocumentViewProps> {
get LayoutFieldKey() {
return this.docView?.LayoutFieldKey || 'layout';
}
- get fitWidth() {
- return this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
+ @computed get fitWidth() {
+ return this.docView?._componentView?.fitWidth?.() ?? this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
+ }
+ @computed get anchorViewDoc() {
+ return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
}
-
@computed get hideLinkButton() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : ''));
}
- linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale;
-
@computed get linkCountView() {
const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
- return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
+ return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
+ }
+ @computed get hidden() {
+ return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
}
-
@computed get docViewPath(): DocumentView[] {
return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
}
@@ -1678,12 +1354,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
-
@computed get panelWidth() {
return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth();
}
@computed get panelHeight() {
- if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
+ if (this.effectiveNativeHeight && (!this.fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) {
return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
}
return this.props.PanelHeight();
@@ -1703,12 +1378,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.dontCenter?.includes('y') ? 0 : this.Yshift;
}
- toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
- getBounds = () => {
- if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+ public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
+ public getBounds = () => {
+ if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
}
- const xf = this.docView?.props
+ const xf = this.docView.props
.ScreenToLocalTransform()
.scale(this.trueNativeWidth() ? this.nativeScaling : 1)
.inverse();
@@ -1745,7 +1420,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Doc.setNativeView(this.props.Document);
custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
};
- switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+ @action
+ switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
setTimeout(
action(() => {
@@ -1765,14 +1441,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}),
this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
);
- });
-
- startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+ };
- @observable textHtmlOverlay: Opt<string>;
- @computed get anchorViewDoc() {
- return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
- }
+ scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale;
docViewPathFunc = () => this.docViewPath;
isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
@@ -1789,11 +1460,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
.translate(-this.centeringX, -this.centeringY)
.scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1);
componentDidMount() {
- this._disposers.reactionScript = reaction(
- () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
+ this._disposers.updateContentsScript = reaction(
+ () => ScriptCast(this.rootDoc.updateContentsScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
output => output
);
this._disposers.height = reaction(
+ // increase max auto height if document has been resized to be greater than current max
() => NumCast(this.layoutDoc._height),
action(height => {
const docMax = NumCast(this.layoutDoc.docMaxAutoHeight);
@@ -1806,24 +1478,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Object.values(this._disposers).forEach(disposer => disposer?.());
!BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
}
- _hoverTimeout: any = undefined;
- isHovering = () => this._isHovering;
- @observable _isHovering = false;
-
- htmlOverlayEffect = '';
@computed get htmlOverlay() {
- const effectProps = {
- delay: 0,
- duration: 500,
- };
- const highlight = (
- <div className="webBox-textHighlight">
- <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
- </div>
- );
return !this.textHtmlOverlay ? null : (
<div className="documentView-htmlOverlay">
- <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div>
+ <div className="documentView-htmlOverlayInner">
+ <Fade delay={0} duration={500}>
+ {DocumentViewInternal.AnimationEffect(
+ <div className="webBox-textHighlight">
+ <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
+ </div>,
+ { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc,
+ this.rootDoc
+ )}{' '}
+ </Fade>
+ </div>
</div>
);
}
@@ -1832,34 +1500,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
TraceMobx();
const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
- const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
- return (
- <div
- className="contentFittingDocumentView"
- onPointerEnter={action(() => {
- clearTimeout(this._hoverTimeout);
- this._isHovering = true;
- })}
- onPointerLeave={action(() => {
- clearTimeout(this._hoverTimeout);
- this._hoverTimeout = setTimeout(
- action(() => (this._isHovering = false)),
- 500
- );
- })}>
+ return this.hidden ? null : (
+ <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
{!this.props.Document || !this.props.PanelWidth() ? null : (
<div
className="contentFittingDocumentView-previewDoc"
ref={this.ContentRef}
style={{
transition: this.props.dataTransition,
- transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
- height:
- isButton || this.props.forceAutoHeight
- ? undefined
- : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
+ transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
+ height: this.props.forceAutoHeight
+ ? undefined
+ : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
}}>
<DocumentViewInternal
{...this.props}
@@ -1872,7 +1526,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
NativeDimScaling={this.NativeDimScaling}
isSelected={this.isSelected}
select={this.select}
- isHovering={this.isHovering}
ScreenToLocalTransform={this.screenToLocalTransform}
focus={this.props.focus || emptyFunction}
ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
@@ -1887,10 +1540,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
}
-export function deiconifyViewFunc(documentView: DocumentView) {
- documentView.iconify();
- //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
-}
ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
documentView.iconify();
documentView.select(false);
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e53422ab7..8d3534a5c 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
select: (isCtrlPressed: boolean) => void;
isContentActive: (outsideReaction?: boolean) => boolean | undefined;
- isDocumentActive?: () => boolean;
+ isDocumentActive?: () => boolean | undefined;
isSelected: (outsideReaction?: boolean) => boolean;
setHeight?: (height: number) => void;
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 75b4907cd..b43e359ff 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -101,7 +101,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
<div
ref={this.createDropTarget}
style={{
- pointerEvents: !this.isContentActive() ? 'all' : undefined,
+ pointerEvents: !this.props.isContentActive() ? 'all' : undefined,
width: this.props.PanelWidth(),
height: this.props.PanelHeight(),
}}>
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 643ad6734..5c220550c 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs, DocUtils } from '../../documents/Documents';
@@ -202,7 +202,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
croppingProto.isPrototype = true;
@@ -222,7 +222,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto.panYMin = anchy / viewScale;
croppingProto.panYMax = anchh / viewScale;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
cropping.y = NumCast(this.rootDoc.y);
this.props.addDocTab(cropping, OpenWhere.inParent);
@@ -368,6 +368,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
}
+ @observable _isHovering = false; // flag to switch between primary and alternate images on hover
@computed get content() {
TraceMobx();
@@ -390,12 +391,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
return (
- <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
+ <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
<div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
{fadepath === srcpath ? null : (
<div
- className={`imageBox-fadeBlocker${(this.props.isHovering?.() && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`}
+ className={`imageBox-fadeBlocker${(this._isHovering && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`}
style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
<img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
</div>
@@ -408,8 +409,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
}
- contentFunc = () => [this.content];
-
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@observable _marqueeing: number[] | undefined;
@@ -472,7 +471,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}}>
<CollectionFreeFormView
ref={this._ffref}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
styleProvider={this.props.styleProvider}
@@ -486,11 +488,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
focus={this.focus}
getScrollHeight={this.getScrollHeight}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
{this.annotationLayer}
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 60417430f..57018fb93 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -3,24 +3,24 @@ 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 { DocCast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
+import { returnAll, returnAlways, returnTrue } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { ImageBox } from './ImageBox';
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';
-import { OpenWhere } from './DocumentView';
export type KVPScript = {
script: CompiledScript;
@@ -30,8 +30,8 @@ export type KVPScript = {
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string) {
- return FieldView.LayoutString(KeyValueBox, fieldStr);
+ public static LayoutString() {
+ return FieldView.LayoutString(KeyValueBox, 'data');
}
private _mainCont = React.createRef<HTMLDivElement>();
@@ -39,13 +39,22 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
private _keyInput = React.createRef<HTMLInputElement>();
private _valInput = React.createRef<HTMLInputElement>();
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+ reverseNativeScaling = returnTrue;
+ able = returnAlways;
+ fitWidth = returnTrue;
+ overridePointerEvents = returnAll;
+ onClickScriptDisable = returnAlways;
+
@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;
+ return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document;
}
@action
@@ -63,9 +72,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
};
public static CompileKVPScript(value: string): KVPScript | undefined {
const eq = value.startsWith('=');
- value = eq ? value.substr(1) : value;
- const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false;
- value = dubEq ? value.substr(2) : value;
+ value = eq ? value.substring(1) : value;
+ const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
+ value = dubEq ? value.substring(2) : value;
const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false };
if (dubEq) options.typecheck = false;
const script = CompileScript(value, options);
@@ -82,8 +91,11 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
} else if (type === 'script') {
field = new ScriptField(script);
} else {
- const res = script.run({ this: target }, console.log);
- if (!res.success) return false;
+ const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
+ if (!res.success) {
+ target[key] = script.originalScript;
+ return true;
+ }
field = res.result;
}
if (Field.IsField(field, true)) {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index e74ef4a39..94434dce7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,19 +1,18 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Field, Opt } from '../../../fields/Doc';
-import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Doc, Field } from '../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { EditableView } from '../EditableView';
+import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { KeyValueBox } from './KeyValueBox';
import './KeyValueBox.scss';
import './KeyValuePair.scss';
import React = require('react');
-import { DefaultStyleProvider } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
// Represents one row in a key value plane
@@ -48,7 +47,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
if (value instanceof Doc) {
e.stopPropagation();
e.preventDefault();
- ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
+ ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, ((OpenWhere.addRight as string) + 'KeyValue') as OpenWhere), icon: 'layer-group' });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
};
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 33fab6873..916458dfd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -79,7 +79,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
@observable _mouseOver = false;
@computed get hoverColor() {
- return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset';
+ return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
}
fitTextToBox = (r: any): any => {
@@ -131,6 +131,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
style={{
backgroundColor: this.hoverColor,
fontSize: StrCast(this.layoutDoc._fontSize),
+ color: StrCast(this.layoutDoc._color),
fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 1b37dc7ab..31f1775e5 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,22 +1,19 @@
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
+import { SelectionManager } from '../../util/SelectionManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkAnchorBox.scss';
import { LinkDocPreview } from './LinkDocPreview';
import React = require('react');
-import { LinkManager } from '../../util/LinkManager';
import globalCssVariables = require('../global/globalCssVariables.scss');
-import { SelectionManager } from '../../util/SelectionManager';
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -31,19 +28,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _x = 0;
@observable _y = 0;
+ @computed get linkSource() {
+ return this.props.docViewPath()[this.props.docViewPath().length - 2].rootDoc; // this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
+ }
+
onPointerDown = (e: React.PointerEvent) => {
- const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
- setupMoveUpEvents(
- this,
- e,
- this.onPointerMove,
- emptyFunction,
- (e, doubleTap) => {
- if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
- else this.props.select(false);
- },
- false
- );
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
+ if (doubleTap) LinkFollower.FollowLink(this.rootDoc, this.linkSource, false);
+ else this.props.select(false);
+ });
};
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
const cdiv = this._ref?.current?.parentElement;
@@ -54,7 +47,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (separation > 100) {
const dragData = new DragManager.DocumentDragData([this.rootDoc]);
dragData.dropAction = 'alias';
- dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'isLinkButton'];
+ dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'onClick'];
DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
return true;
} else {
@@ -73,7 +66,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView
const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100);
const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100);
- const linkSource = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1';
const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
@@ -87,7 +79,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerEnter={e =>
LinkDocPreview.SetLinkInfo({
docProps: this.props,
- linkSrc: linkSource,
+ linkSrc: this.linkSource,
linkDoc: this.rootDoc,
showHeader: true,
location: [e.clientX, e.clientY + 20],
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 43f4b43fb..46ccdecae 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,29 +1,38 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import { emptyFunction, returnFalse } from "../../../Utils";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { StyleProp } from "../StyleProvider";
-import { ComparisonBox } from "./ComparisonBox";
+import React = require('react');
+import { observer } from 'mobx-react';
+import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkBox.scss";
+import './LinkBox.scss';
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
- isContentActiveFunc = () => this.isContentActive();
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(LinkBox, fieldKey);
+ }
+
+ onClickScriptDisable = returnAlways;
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
render() {
- if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
- return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
- style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} >
- <ComparisonBox {...this.props}
- fieldKey="anchor"
- setHeight={emptyFunction}
- dontRegisterView={true}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActiveFunc}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- moveDocument={returnFalse} />
- </div>;
+ if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
+ return (
+ <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
+ <ComparisonBox
+ {...this.props}
+ fieldKey="anchor"
+ setHeight={emptyFunction}
+ dontRegisterView={true}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.props.isContentActive}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 16b352e4f..fcc5b6975 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -171,7 +171,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
followLink = () => {
LinkDocPreview.Clear();
if (this._linkDoc && this._linkSrc) {
- LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
+ LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
} else if (this.props.hrefs?.length) {
const webDoc =
Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ??
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 0064a2022..36be7d257 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -8,7 +8,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -616,7 +616,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// zoom: 15,
// });
}}
- style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
@@ -635,7 +635,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.map(marker => (
<MapBoxInfoWindow
key={marker[Id]}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
place={marker}
markerMap={this.markerMap}
PanelWidth={this.infoWidth}
@@ -674,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ usePanelWidth={true}
showSidebar={this.SidebarShown}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 00bedafbe..d65ef9c4c 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -52,7 +52,8 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
<div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
<CollectionStackingView
ref={r => (this._stack = r)}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
Document={this.props.place}
DataDoc={undefined}
fieldKey="data"
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 7e0b5c4d3..6f67e8d70 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,16 +5,20 @@ import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { InkTool } from '../../../fields/InkField';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { KeyCodes } from '../../util/KeyCodes';
+import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -41,7 +45,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
private _searchRef = React.createRef<HTMLInputElement>();
- private _selectReactionDisposer: IReactionDisposer | undefined;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _sidebarRef = React.createRef<SidebarAnnos>();
@observable private _searching: boolean = false;
@@ -115,7 +119,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw;
cropping._height = anchh;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
croppingProto.isPrototype = true;
@@ -126,7 +130,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
croppingProto['data-nativeWidth'] = anchw;
croppingProto['data-nativeHeight'] = anchh;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
}
this.props.bringToFront(cropping);
@@ -184,11 +188,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
componentWillUnmount() {
- this._selectReactionDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
}
componentDidMount() {
this.props.setContentView?.(this);
- this._selectReactionDisposer = reaction(
+ this._disposers.select = reaction(
() => this.props.isSelected(),
() => {
document.removeEventListener('keydown', this.onKeyDown);
@@ -196,6 +200,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.rootDoc.scrollTop,
+ () => {
+ if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '-panY'])) instanceof ComputedField)) {
+ this.props.Document[this.SidebarKey + '-panY'] = ComputedField.MakeFunction('this.scrollTop');
+ }
+ this.props.Document[this.SidebarKey + '-viewScale'] = 1;
+ this.props.Document[this.SidebarKey + '-panX'] = 0;
+ }
+ );
}
brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
@@ -303,7 +317,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// adding external documents; to sidebar key
// if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
- sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
};
@@ -330,7 +344,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
},
(e, movement, isClick) => !isClick && batch.end(),
() => {
- this.toggleSidebar();
+ onButton && this.toggleSidebar();
batch.end();
}
);
@@ -426,12 +440,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
+ @undoBatch
+ toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
specificContextMenu = (e: React.MouseEvent): void => {
- const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
- funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
- //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
+ !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
+ //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+ !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
+ const help = cm.findByDescription('Help...');
+ const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+ helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+ !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
};
@computed get renderTitleBox() {
@@ -467,14 +489,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
+ public get SidebarKey() {
+ return this.fieldKey + '-sidebar';
+ }
+ @computed get pdfScale() {
+ const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth);
+ const pdfRatio = pdfNativeWidth / nativeWidth;
+ return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth;
+ }
+ @computed get sidebarNativeWidth() {
+ return this.sidebarWidth() / this.pdfScale;
+ }
+ @computed get sidebarNativeHeight() {
+ return this.props.PanelHeight() / this.pdfScale;
+ }
+ sidebarNativeWidthFunc = () => this.sidebarNativeWidth;
+ sidebarNativeHeightFunc = () => this.sidebarNativeHeight;
+ sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
+ sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
+ sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0);
+ @computed get sidebarCollection() {
+ const renderComponent = (tag: string) => {
+ const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView;
+ return ComponentTag === CollectionStackingView ? (
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ setHeight={emptyFunction}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ />
+ ) : (
+ <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+ <ComponentTag
+ {...this.props}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ NativeWidth={this.sidebarNativeWidthFunc}
+ NativeHeight={this.sidebarNativeHeightFunc}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.sidebarWidth}
+ xPadding={0}
+ yPadding={0}
+ viewField={this.SidebarKey}
+ isAnnotationOverlay={false}
+ originTopLeft={true}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ select={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.sidebarRemDocument}
+ moveDocument={this.sidebarMoveDocument}
+ addDocument={this.sidebarAddDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ renderDepth={this.props.renderDepth + 1}
+ noSidebar={true}
+ fieldKey={this.SidebarKey}
+ />
+ </div>
+ );
+ };
+ return (
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
+ {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ </div>
+ );
+ }
isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
- return (
+ return !this._pdf ? null : (
<div
- className={'pdfBox'}
+ className="pdfBox"
onContextMenu={this.specificContextMenu}
style={{
display: this.props.thumbShown?.() ? 'none' : undefined,
@@ -497,7 +592,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
addDocTab={this.sidebarAddDocTab}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- pdf={this._pdf!}
+ pdf={this._pdf}
focus={this.focus}
url={this.pdfUrl!.url.pathname}
isContentActive={this.isPdfContentActive}
@@ -511,22 +606,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
crop={this.crop}
/>
</div>
- <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
- <SidebarAnnos
- ref={this._sidebarRef}
- {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- setHeight={emptyFunction}
- nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
- showSidebar={this.SidebarShown}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- sidebarAddDocument={this.sidebarAddDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- />
- </div>
+ <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
{this.settingsPanel()}
</div>
);
@@ -536,21 +616,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
render() {
TraceMobx();
- if (this._pdf) {
- if (!this.props.thumbShown?.()) {
- return this.renderPdfView;
- }
- return null;
- }
+ if (this.props.thumbShown?.()) return null;
+ const pdfView = this.renderPdfView;
const href = this.pdfUrl?.url.href;
- if (href) {
+ if (!pdfView && href) {
if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
else {
if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
}
}
- return this.renderTitleBox;
+ return pdfView ?? this.renderTitleBox;
}
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index ec5917b9e..6efe62e0b 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,32 +1,31 @@
import * as React from 'react';
-import "./RecordingView.scss";
-import { useEffect, useRef, useState } from "react";
-import { ProgressBar } from "./ProgressBar"
+import './RecordingView.scss';
+import { useEffect, useRef, useState } from 'react';
+import { ProgressBar } from './ProgressBar';
import { MdBackspace } from 'react-icons/md';
import { FaCheckCircle } from 'react-icons/fa';
-import { IconContext } from "react-icons";
+import { IconContext } from 'react-icons';
import { Networking } from '../../../Network';
import { Upload } from '../../../../server/SharedMediaTypes';
import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
import { Presentation, TrackMovements } from '../../../util/TrackMovements';
export interface MediaSegment {
- videoChunks: any[],
- endTime: number,
- startTime: number,
- presentation?: Presentation,
+ videoChunks: any[];
+ endTime: number;
+ startTime: number;
+ presentation?: Presentation;
}
interface IRecordingViewProps {
- setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
- setDuration: (seconds: number) => void
- id: string
+ setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void;
+ setDuration: (seconds: number) => void;
+ id: string;
}
const MAXTIME = 100000;
export function RecordingView(props: IRecordingViewProps) {
-
const [recording, setRecording] = useState(false);
const recordingTimerRef = useRef<number>(0);
const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
@@ -46,19 +45,16 @@ export function RecordingView(props: IRecordingViewProps) {
const [finished, setFinished] = useState<boolean>(false);
const [trackScreen, setTrackScreen] = useState<boolean>(false);
-
-
const DEFAULT_MEDIA_CONSTRAINTS = {
video: {
width: 1280,
height: 720,
-
},
audio: {
echoCancellation: true,
noiseSuppression: true,
- sampleRate: 44100
- }
+ sampleRate: 44100,
+ },
};
useEffect(() => {
@@ -71,12 +67,11 @@ export function RecordingView(props: IRecordingViewProps) {
const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
// upload the segments to the server and get their server access paths
- const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
- .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+ const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server));
// concat the segments together using post call
const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
- !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+ !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed');
})();
}
}, [videos]);
@@ -87,7 +82,9 @@ export function RecordingView(props: IRecordingViewProps) {
}, [finished]);
// check if the browser supports media devices on first load
- useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+ useEffect(() => {
+ if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.');
+ }, []);
useEffect(() => {
let interval: any = null;
@@ -102,24 +99,24 @@ export function RecordingView(props: IRecordingViewProps) {
}, [recording]);
useEffect(() => {
- setVideoProgressHelper(recordingTimer)
+ setVideoProgressHelper(recordingTimer);
recordingTimerRef.current = recordingTimer;
}, [recordingTimer]);
const setVideoProgressHelper = (progress: number) => {
const newProgress = (progress / MAXTIME) * 100;
setProgress(newProgress);
- }
+ };
const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- videoElementRef.current!.src = "";
+ videoElementRef.current!.src = '';
videoElementRef.current!.srcObject = stream;
videoElementRef.current!.muted = true;
return stream;
- }
+ };
const record = async () => {
// don't need to start a new stream every time we start recording a new segment
@@ -145,29 +142,28 @@ export function RecordingView(props: IRecordingViewProps) {
const nextVideo = {
videoChunks,
endTime: recordingTimerRef.current,
- startTime: videos?.lastElement()?.endTime || 0
+ startTime: videos?.lastElement()?.endTime || 0,
};
// depending on if a presenation exists, add it to the video
const presentation = TrackMovements.Instance.yieldPresentation();
- setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+ setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]);
}
// reset the temporary chunks
videoChunks = [];
setRecording(false);
- }
+ };
videoRecorder.current.start(200);
- }
-
+ };
// if this is called, then we're done recording all the segments
const finish = (e: React.PointerEvent) => {
e.stopPropagation();
// call stop on the video recorder if active
- videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+ videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop();
// end the streams (audio/video) to remove recording icon
const stream = videoElementRef.current!.srcObject;
@@ -178,94 +174,91 @@ export function RecordingView(props: IRecordingViewProps) {
// this will call upon progessbar to update videos to be in the correct order
setFinished(true);
- }
+ };
const pause = (e: React.PointerEvent) => {
e.stopPropagation();
// if recording, then this is just a new segment
- videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
- }
+ videoRecorder.current?.state === 'recording' && videoRecorder.current.stop();
+ };
const start = (e: React.PointerEvent) => {
- setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
- // start recording if not already recording
- if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
-
- return true; // cancels propagation to documentView to avoid selecting it.
- }, false, false);
- }
+ setupMoveUpEvents(
+ {},
+ e,
+ returnTrue,
+ returnFalse,
+ e => {
+ // start recording if not already recording
+ if (!videoRecorder.current || videoRecorder.current.state === 'inactive') record();
+
+ return true; // cancels propagation to documentView to avoid selecting it.
+ },
+ false,
+ false
+ );
+ };
const undoPrevious = (e: React.PointerEvent) => {
e.stopPropagation();
setDoUndo(prev => !prev);
- }
+ };
- const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+ const handleOnTimeUpdate = () => {
+ playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
+ };
const millisecondToMinuteSecond = (milliseconds: number) => {
const toTwoDigit = (digit: number) => {
- return String(digit).length == 1 ? "0" + digit : digit
- }
+ return String(digit).length == 1 ? '0' + digit : digit;
+ };
const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
- return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
- }
+ return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds);
+ };
return (
<div className="recording-container">
<div className="video-wrapper">
- <video id={`video-${props.id}`}
- autoPlay
- muted
- onTimeUpdate={() => handleOnTimeUpdate()}
- ref={videoElementRef}
- />
+ <video id={`video-${props.id}`} autoPlay muted onTimeUpdate={() => handleOnTimeUpdate()} ref={videoElementRef} />
<div className="recording-sign">
<span className="dot" />
<p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
</div>
<div className="controls">
-
<div className="controls-inner-container">
- <div className="record-button-wrapper">
- {recording ?
- <button className="stop-button" onPointerDown={pause} /> :
- <button className="record-button" onPointerDown={start} />
- }
- </div>
-
- {!recording && (videos.length > 0 ?
-
- <div className="options-wrapper video-edit-wrapper">
- <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
- <MdBackspace onPointerDown={undoPrevious} />
- </IconContext.Provider>
- <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
- <FaCheckCircle onPointerDown={finish} />
- </IconContext.Provider>
- </div>
-
- : <div className="options-wrapper track-screen-wrapper">
- <label className="track-screen">
- <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
- <span className="checkmark"></span>
- Track Screen
- </label>
- </div>)}
-
+ <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div>
+
+ {!recording &&
+ (videos.length > 0 ? (
+ <div className="options-wrapper video-edit-wrapper">
+ <IconContext.Provider value={{ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }}>
+ <MdBackspace onPointerDown={undoPrevious} />
+ </IconContext.Provider>
+ <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}>
+ <FaCheckCircle onPointerDown={finish} />
+ </IconContext.Provider>
+ </div>
+ ) : (
+ <div className="options-wrapper track-screen-wrapper">
+ <label className="track-screen">
+ <input
+ type="checkbox"
+ checked={trackScreen}
+ onChange={e => {
+ setTrackScreen(e.target.checked);
+ }}
+ />
+ <span className="checkmark"></span>
+ Track Screen
+ </label>
+ </div>
+ ))}
</div>
-
</div>
- <ProgressBar
- videos={videos}
- setVideos={setVideos}
- orderVideos={orderVideos}
- progress={progress}
- recording={recording}
- doUndo={doUndo}
- setCanUndo={setCanUndo}
- />
+ <ProgressBar videos={videos} setVideos={setVideos} orderVideos={orderVideos} progress={progress} recording={recording} doUndo={doUndo} setCanUndo={setCanUndo} />
</div>
- </div>)
-} \ No newline at end of file
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 61e4894f0..db11a7776 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast } from '../../../fields/Types';
import { AudioField, VideoField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
@@ -277,7 +277,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
};
- contentFunc = () => [this.threed, this.content];
videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
@@ -287,7 +286,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
<div className="videoBox-viewer">
<div style={{ position: 'relative', height: this.videoPanelHeight() }}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
PanelHeight={this.videoPanelHeight}
PanelWidth={this.props.PanelWidth}
focus={this.props.focus}
@@ -296,6 +298,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
select={emptyFunction}
isContentActive={returnFalse}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={emptyFunction}
removeDocument={returnFalse}
moveDocument={returnFalse}
@@ -304,17 +307,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- {this.contentFunc}
+ <>
+ {this.threed}
+ {this.content}
+ </>
</CollectionFreeFormView>
</div>
<div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
{!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
<FormattedTextBox
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
Document={this.dataDoc[this.fieldKey + '-dictation']}
fieldKey={'text'}
PanelHeight={this.formattedPanelHeight}
- isAnnotationOverlay={true}
select={emptyFunction}
isContentActive={emptyFunction}
NativeDimScaling={returnOne}
@@ -324,7 +329,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
- CollectionView={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
)}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index fa2021642..f09962b22 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyString } from '../../../Utils';
+import { returnAlways, returnEmptyString, returnTrue } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
import { CompileScript, ScriptParam } from '../../util/Scripting';
@@ -114,6 +114,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}
}
+ onClickScriptDisable = returnAlways;
+
@action
componentDidMount() {
this.props.setContentView?.(this);
@@ -180,13 +182,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const params: ScriptParam = {};
this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim()));
- const result = CompileScript(this.rawText, {
- editable: true,
- transformer: DocumentIconContainer.getTransformer(),
- params,
- typecheck: false,
- });
- Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true);
+ const result = !this.rawText.trim()
+ ? ({ compiled: false, errors: undefined } as any)
+ : CompileScript(this.rawText, {
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer(),
+ params,
+ typecheck: false,
+ });
+ Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true);
this.onError(result.compiled ? undefined : result.errors);
return result.compiled;
};
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index dc6bf6f9e..b144c9318 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
@@ -33,6 +33,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
+import { ScriptField } from '../../../fields/ScriptField';
+import { FollowLinkScript } from '../../util/LinkFollower';
const path = require('path');
/**
@@ -324,10 +326,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_width: 150,
_height: 50,
title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' });
Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
timecode: this.layoutDoc._currentTimecode,
@@ -335,7 +337,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
this.props.removeDocument?.(b);
- this.createRealSummaryLink(resolved);
+ this.createSnapshotLink(resolved);
}
});
} else {
@@ -345,7 +347,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
}
};
@@ -359,26 +361,26 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
};
// creates link for snapshot
- createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
const width = NumCast(this.layoutDoc._width) || 1;
const height = NumCast(this.layoutDoc._height);
- const imageSummary = Docs.Create.ImageDocument(url, {
+ const imageSnapshot = Docs.Create.ImageDocument(url, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc),
_nativeHeight: Doc.NativeHeight(this.layoutDoc),
x: NumCast(this.layoutDoc.x) + width,
y: NumCast(this.layoutDoc.y),
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
_width: 150,
_height: (height / width) * 150,
title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
});
- Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
- Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor(true) }, 'video snapshot');
+ Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSnapshot);
+ const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' });
link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
};
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
@@ -894,8 +896,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
playing = () => this._playing;
- contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
scaling = () => this.props.NativeDimScaling?.() || 1;
panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
@@ -1012,7 +1012,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
cropping.timecodeToHide = undefined;
cropping.timecodeToShow = undefined;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
croppingProto.isPrototype = true;
@@ -1033,7 +1033,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto.panYMin = anchy / viewScale;
croppingProto.panYMax = anchh / viewScale;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
}
this.props.bringToFront(cropping);
return cropping;
@@ -1068,7 +1068,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
left: (this.props.PanelWidth() - this.panelWidth()) / 2,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
CollectionView={undefined}
@@ -1076,6 +1079,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
annotationLayerHostsContent={true}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ isAnyChildContentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocalTransform}
docFilters={this.timelineDocFilter}
select={emptyFunction}
@@ -1084,7 +1088,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
+ {this.youtubeVideoId ? this.youtubeContent : this.content}
</CollectionFreeFormView>
</div>
{this.annotationLayer}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index abde5a9ea..9ed7081da 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -206,7 +206,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._webPageHasBeenRendered = true;
} else if (
(!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
LightboxView.LightboxDoc !== this.rootDoc
) {
// don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
@@ -307,7 +306,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
if (scrollTo !== undefined) {
if (this._initialScroll === undefined) {
- this.goTo(scrollTo, options.zoomTime ?? 500, options.easeFunc);
+ const focusTime = options.zoomTime ?? 500;
+ this.goTo(scrollTo, focusTime, options.easeFunc);
+ return focusTime;
} else {
this._initialScroll = scrollTo;
}
@@ -884,7 +885,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
@computed get content() {
- const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
+ const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
return (
<div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
{this.urlContent}
@@ -912,9 +913,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
- const renderAnnotations = (docFilters?: () => string[]) => (
+ const renderAnnotations = (docFilters: () => string[]) => (
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.annotationKey}
@@ -929,6 +933,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
dropAction={'alias'}
docFilters={docFilters}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
deleted file mode 100644
index b4a382faf..000000000
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { Colors } from "../../global/globalEnums";
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function changeView(view: string) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent";
- selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
-}); \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index a1ca777b3..f3b43501b 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -43,10 +43,6 @@
cursor: pointer;
flex-direction: column;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
svg {
width: 50% !important;
height: 50%;
@@ -68,10 +64,6 @@
justify-content: center;
align-items: center;
justify-items: center;
-
- &:hover {
- filter: brightness(0.85) !important;
- }
}
&.tglBtn,
@@ -166,7 +158,7 @@
width: 100%;
border-radius: 100%;
flex-direction: column;
- margin-top: -4px;
+ // margin-top: -4px;
svg {
width: 60% !important;
@@ -220,10 +212,6 @@
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
}
&.colorBtnLabel {
@@ -248,10 +236,6 @@
align-content: center;
align-items: center;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
.menuButton-dropdownList {
position: absolute;
width: 150px;
@@ -283,10 +267,6 @@
cursor: pointer;
background: transparent;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
&.slider {
color: $white;
cursor: pointer;
@@ -447,11 +427,11 @@
}
.dropbox-background {
- width: 100vw;
- height: 100vh;
- top: 0;
+ width: 200vw;
+ height: 200vh;
+ top: -100vh;
z-index: 20;
- left: 0;
+ left: -100vw;
background: transparent;
position: fixed;
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index b3a3c3ae4..8eacfbc51 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,5 +1,4 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faAlignRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, observable, runInAction } from 'mobx';
@@ -101,7 +100,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
}
Icon = (color: string) => {
- const icon = StrCast(this.dataDoc.icon, 'user') as any;
+ const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? 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} />;
};
@@ -162,7 +161,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>
);
return (
- <div className={`menuButton ${this.type} ${numBtnType}`} onPointerDown={e => e.stopPropagation()} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ <div
+ className={`menuButton ${this.type} ${numBtnType}`}
+ onPointerDown={e => e.stopPropagation()}
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
{checkResult}
{label}
{this.rootDoc.dropDownOpen ? dropdown : null}
@@ -199,7 +204,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
e.stopPropagation();
e.preventDefault();
}}
- onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
<input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} />
</div>
<div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
@@ -216,6 +224,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
onClick={e => {
e.stopPropagation();
this.rootDoc.dropDownOpen = false;
+ Doc.UnBrushAllDocs();
}}
/>
</div>
@@ -238,7 +247,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
{this.Icon(color)}
{!this.label || !FontIconBox.GetShowLabels() ? null : (
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
@@ -317,7 +329,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }}
- onClick={dropdown ? () => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen) : undefined}>
+ onClick={
+ dropdown
+ ? () => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ }
+ : undefined
+ }>
{dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />}
<div className="menuButton-dropdown-header">{text && text[0].toUpperCase() + text.slice(1)}</div>
{label}
@@ -336,6 +355,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
onClick={e => {
e.stopPropagation();
this.rootDoc.dropDownOpen = false;
+ Doc.UnBrushAllDocs();
}}
/>
</div>
@@ -380,6 +400,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(e => {
this.colorPickerClosed = !this.colorPickerClosed;
+ setTimeout(() => Doc.UnBrushAllDocs());
e.stopPropagation();
})}
onPointerDown={e => e.stopPropagation()}>
@@ -398,6 +419,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
e.preventDefault();
e.stopPropagation();
this.colorPickerClosed = true;
+ Doc.UnBrushAllDocs();
})}
/>
</div>
@@ -588,15 +610,52 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
});
+ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snap lines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ // prettier-ignore
+ const map: Map<'grid' | 'snap lines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+ ['grid', {
+ undo: false,
+ checkResult: (doc:Doc) => doc._backgroundGridShow,
+ setDoc: (doc:Doc) => doc._backgroundGridShow = !doc._backgroundGridShow,
+ }],
+ ['snap lines', {
+ undo: false,
+ checkResult: (doc:Doc) => doc.showSnapLines,
+ setDoc: (doc:Doc) => doc._showSnapLines = !doc._showSnapLines,
+ }],
+ ['viewAll', {
+ undo: false,
+ checkResult: (doc:Doc) => doc._fitContentsToBox,
+ setDoc: (doc:Doc) => doc._fitContentsToBox = !doc._fitContentsToBox,
+ }],
+ ['clusters', {
+ undo: false,
+ checkResult: (doc:Doc) => doc._useClusters,
+ setDoc: (doc:Doc) => doc._useClusters = !doc._useClusters,
+ }],
+ ['arrange', {
+ undo: true,
+ checkResult: (doc:Doc) => doc._autoArrange,
+ setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
+ }],
+ ]);
+
+ if (checkResult) {
+ return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
+ }
+ const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} };
+ SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
+ setTimeout(() => batch.end(), 100);
+});
ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
const selected = SelectionManager.Docs().lastElement();
// prettier-ignore
- const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void; setMode?: () => void }> = new Map([
+ const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void;}> = new Map([
['font', {
checkResult: () => RichTextMenu.Instance?.fontFamily,
setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
- setMode: () => Doc.UserDoc().textAlign = value,
}],
['highlight', {
checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
@@ -619,8 +678,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
if (checkResult) {
return map.get(attr)?.checkResult();
}
- if (editorView?.state) map.get(attr)?.setDoc();
- else map.get(attr)?.setMode?.();
+ map.get(attr)?.setDoc?.();
});
type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
@@ -781,7 +839,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | '
setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
}],
['fillColor', {
- checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'),
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"),
setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
setMode: () => SetActiveFillColor(StrCast(value)),
}],
@@ -791,7 +849,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | '
setMode: () => SetActiveInkWidth(value.toString()),
}],
['strokeColor', {
- checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.color) : ActiveInkColor()),
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
setInk: (doc: Doc) => (doc.color = String(value)),
setMode: () => SetActiveInkColor(StrCast(value)),
}],
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
index 7f414ddbb..74c3c563c 100644
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
@@ -12,7 +12,7 @@ export class ColorDropdown extends Component<IButtonProps> {
const active: string = StrCast(this.props.rootDoc.dropDownOpen);
const script: string = StrCast(this.props.rootDoc.script);
- const scriptCheck: string = script + "(undefined, true)";
+ const scriptCheck: string = script + '(undefined, true)';
const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
const stroke: boolean = false;
@@ -24,24 +24,21 @@ export class ColorDropdown extends Component<IButtonProps> {
// strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />);
// }
- const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
- '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
- '#FFFFFF', '#f1efeb'];
+ const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb'];
- const colorBox = (func: (color: ColorState) => void) => <SketchPicker
- disableAlpha={!stroke}
- onChange={func} color={boolResult ? boolResult : "#FFFFFF"}
- presetColors={colorOptions} />;
- const label = !this.props.label || !FontIconBox.GetShowLabels() ? (null) :
- <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: "absolute" }}>
- {this.props.label}
- </div>;
+ const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />;
+ const label =
+ !this.props.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}>
+ {this.props.label}
+ </div>
+ );
- const dropdownCaret = <div
- className="menuButton-dropDown"
- style={{ borderBottomRightRadius: active ? 0 : undefined }}>
- <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" />
- </div>;
+ const dropdownCaret = (
+ <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}>
+ <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" />
+ </div>
+ );
const click = (value: ColorState) => {
const hex: string = value.hex;
@@ -51,26 +48,30 @@ export class ColorDropdown extends Component<IButtonProps> {
}
};
return (
- <div className={`menuButton ${this.props.type} ${active}`}
+ <div
+ className={`menuButton ${this.props.type} ${active}`}
style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }}
- onClick={() => this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen}
+ onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)}
onPointerDown={e => e.stopPropagation()}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} />
- <div className="colorButton-color"
- style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />
+ <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} />
{label}
{/* {dropdownCaret} */}
- {this.props.rootDoc.dropDownOpen ?
+ {this.props.rootDoc.dropDownOpen ? (
<div>
- <div className="menuButton-dropdownBox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}>
+ <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
{colorBox(click)}
</div>
- <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.props.rootDoc.dropDownOpen = false; }} />
+ <div
+ className="dropbox-background"
+ onClick={e => {
+ e.stopPropagation();
+ this.props.rootDoc.dropDownOpen = false;
+ }}
+ />
</div>
- : null}
+ ) : null}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index fcd6e0c55..aa269d8d6 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -32,7 +32,7 @@ export class DashDocCommentView {
};
this.root = ReactDOM.createRoot(this.dom);
- this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
+ this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
(this as any).dom = this.dom;
}
@@ -48,7 +48,7 @@ export class DashDocCommentView {
}
interface IDashDocCommentViewInternal {
- docid: string;
+ docId: string;
view: any;
getPos: any;
}
@@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
}
onPointerLeaveCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
e.preventDefault();
e.stopPropagation();
}
onPointerEnterCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
e.preventDefault();
e.stopPropagation();
}
@@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
setTimeout(() => {
- expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try {
this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
} catch (e) {}
@@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const state = this.props.view.state;
for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) {
const m = state.doc.nodeAt(i);
- if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) {
+ if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) {
return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean };
}
}
- const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' });
+ const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' });
this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc));
setTimeout(() => {
try {
@@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
return (
<span
className="formattedTextBox-inlineComment"
- id={'DashDocCommentView-' + this.props.docid}
+ id={'DashDocCommentView-' + this.props.docId}
onPointerLeave={this.onPointerLeaveCollapsed}
onPointerEnter={this.onPointerEnterCollapsed}
onPointerUp={this.onPointerUpCollapsed}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 19ee1fc7b..c00ab6a7e 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -42,7 +42,7 @@ export class DashDocView {
this.root = ReactDOM.createRoot(this.dom);
this.root.render(
- <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
+ <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
);
}
destroy() {
@@ -53,7 +53,7 @@ export class DashDocView {
}
interface IDashDocViewInternal {
- docid: string;
+ docId: string;
alias: string;
tbox: FormattedTextBox;
width: string;
@@ -77,7 +77,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
updateDoc = action((dashDoc: Doc) => {
this._dashDoc = dashDoc;
- this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+ this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc);
if (this._finalLayout) {
if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
@@ -107,12 +107,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
super(props);
this._textBox = this.props.tbox;
- DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
+ DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
this.props.alias &&
- DocServer.GetRefField(this.props.docid).then(async dashDocBase => {
+ DocServer.GetRefField(this.props.docId).then(async dashDocBase => {
if (dashDocBase instanceof Doc) {
- const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias);
aliasedDoc.layoutKey = 'layout';
this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
this.updateDoc(aliasedDoc);
@@ -161,12 +161,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
};
onPointerLeave = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = '');
};
onPointerEnter = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = 'orange');
};
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index a89e8b4ed..f23426bb3 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -54,7 +54,7 @@ export class DashFieldView {
unclickable={this.unclickable}
getPos={getPos}
fieldKey={node.attrs.fieldKey}
- docid={node.attrs.docid}
+ docId={node.attrs.docId}
width={node.attrs.width}
height={node.attrs.height}
hideKey={node.attrs.hideKey}
@@ -76,7 +76,7 @@ export class DashFieldView {
interface IDashFieldViewInternal {
fieldKey: string;
- docid: string;
+ docId: string;
hideKey: boolean;
tbox: FormattedTextBox;
width: number;
@@ -100,8 +100,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
this._fieldKey = this.props.fieldKey;
this._textBoxDoc = this.props.tbox.props.Document;
- if (this.props.docid) {
- DocServer.GetRefField(this.props.docid).then(
+ if (this.props.docId) {
+ DocServer.GetRefField(this.props.docId).then(
action(async dashDoc => {
dashDoc instanceof Doc && (this._dashDoc = dashDoc);
})
@@ -116,7 +116,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
public static multiValueDelimeter = ';';
public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) {
- const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? (fieldKey === 'PARAMS' ? textBoxDoc[fieldKey] : '');
+ const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? '';
const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal;
return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' };
}
@@ -223,7 +223,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true);
} else {
const splits = newText.split(DashFieldViewInternal.multiValueDelimeter);
- if (this._fieldKey !== 'PARAMS' || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) {
+ if (!this._textBoxDoc[this._fieldKey]) {
const strVal = splits.length > 1 ? new List<string>(splits) : newText;
if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal;
Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3eae47f49..f6fdfbe29 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -18,10 +18,10 @@ import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from '../../../../fields/RichTextField';
import { RichTextUtils } from '../../../../fields/RichTextUtils';
-import { ComputedField } from '../../../../fields/ScriptField';
+import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
@@ -67,6 +67,7 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { IsFollowLinkScript } from '../../../util/LinkFollower';
const translateGoogleApi = require('translate-google-api');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -191,7 +192,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return '';
}
public static GetDocFromUrl(url: string) {
- return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid
+ return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
}
constructor(props: any) {
@@ -459,7 +460,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
alink =
alink ??
(LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+ DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!);
newAutoLinks.add(alink);
const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -550,7 +551,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
width: target[WidthSym](),
height: target[HeightSym](),
title: 'dashDoc',
- docid: target[Id],
+ docId: target[Id],
float: 'unset',
});
if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
@@ -1332,7 +1333,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
if (pdfAnchor instanceof Doc) {
const dashField = view.state.schema.nodes.paragraph.create({}, [
- view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
view.state.schema.marks.linkAnchor.create({
allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }],
location: 'add:right',
@@ -1345,7 +1346,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
]),
]);
- const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted');
+ const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' });
if (link) {
view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
}
@@ -1542,22 +1543,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// but that's changed, so this shouldn't be needed.
//e.stopPropagation(); // if the text box is selected, then it consumes all down events
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
onSelectEnd = (e: PointerEvent) => {
document.removeEventListener('pointerup', this.onSelectEnd);
- document.removeEventListener('pointermove', this.onSelectMove);
};
onPointerUp = (e: React.PointerEvent): void => {
const editor = this._editorView!;
const state = editor?.state;
- if (!state || !editor) return;
+ if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
else if (this.props.isContentActive(true)) {
const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
@@ -1569,10 +1567,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return;
}
}
-
- if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
- e.stopPropagation();
- }
};
@action
onDoubleClick = (e: React.MouseEvent): void => {
@@ -1607,6 +1601,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@observable public static Focused: FormattedTextBox | undefined;
onClick = (e: React.MouseEvent): void => {
+ if (!this.props.isContentActive()) return;
if ((e.nativeEvent as any).handledByInnerReactInstance) {
e.stopPropagation();
return;
@@ -1638,8 +1633,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this.props.isSelected(true)) {
// if text box is selected, then it consumes all click events
(e.nativeEvent as any).handledByInnerReactInstance = true;
- if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it.
- // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment)
this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
}
this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
@@ -1741,8 +1734,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
+ if ((e.altKey || e.ctrlKey) && e.key === 't') {
e.preventDefault();
+ e.stopPropagation();
+ this.props.setTitleFocus?.();
return;
}
const state = this._editorView!.state;
@@ -1793,6 +1788,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._ignoreScroll = true;
this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop;
this._ignoreScroll = false;
+ e.stopPropagation();
+ e.preventDefault();
}
}
};
@@ -1815,12 +1812,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
};
- fitContentsToBox = () => this.props.Document._fitContentsToBox;
+ fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox);
sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
- // console.log("printting allSideBarDocs");
- // console.log(this.allSidebarDocs);
return this.addDocument(doc, sidebarKey);
};
sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
@@ -1861,8 +1856,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
className="formattedTextBox-sidebar-handle"
onPointerDown={this.sidebarDown}
style={{
- backgroundColor: backgroundColor,
- color: color,
+ backgroundColor,
+ color,
opacity: annotated ? 1 : undefined,
}}>
<FontAwesomeIcon icon={'comment-alt'} />
@@ -1876,33 +1871,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
- fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- ScreenToLocalTransform={this.sidebarScreenToLocal}
+ usePanelWidth={true}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
showSidebar={this.SidebarShown}
- PanelWidth={this.sidebarWidth}
- setHeight={this.setSidebarHeight}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ fieldKey={this.fieldKey}
+ PanelWidth={this.sidebarWidth}
+ setHeight={this.setSidebarHeight}
/>
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
<ComponentTag
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.sidebarWidth}
xPadding={0}
yPadding={0}
- scaleField={this.SidebarKey + '-scale'}
+ viewField={this.SidebarKey}
isAnnotationOverlay={false}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
NativeDimScaling={this.sidebarContentScaling}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.sidebarRemDocument}
@@ -1943,7 +1941,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return styleFromLayoutString?.height === '0px' ? null : (
<div
className="formattedTextBox-cont"
- onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+ (e: WheelEvent) => {
+ if (this.props.isContentActive()) {
+ if (!NumCast(this.layoutDoc._scrollTop) && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+ { passive: false }
+ )
+ }
style={{
...(this.props.dontScale
? {}
@@ -1997,7 +2006,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined,
+ pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined,
}}
/>
</div>
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 5675776fb..e691869cc 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -89,8 +89,8 @@ export class RichTextRules {
textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
textDoc[inlineFieldKey] = ''; // set a default value for the annotation
const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' });
+ const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
const sm = state.storedMarks || undefined;
const replaced = node
? state.tr
@@ -248,18 +248,18 @@ export class RichTextRules {
// [[fieldKey:Doc]] => show field of doc
new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
const fieldKey = match[1];
- const docid = match[3]?.replace(':', '');
+ const docId = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
if (!fieldKey) {
- if (docid) {
- DocServer.GetRefField(docid).then(docx => {
+ if (docId) {
+ DocServer.GetRefField(docId).then(docx => {
const rstate = this.TextBox.EditorView?.state;
const selection = rstate?.selection.$from.pos;
if (rstate) {
this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
}
- const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
+ const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
+ DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' });
const fstate = this.TextBox.EditorView?.state;
if (fstate && selection) {
@@ -274,7 +274,7 @@ export class RichTextRules {
const num = value.match(/^[0-9.]$/);
this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false });
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
}),
@@ -311,16 +311,16 @@ export class RichTextRules {
const fieldKey = match[1] || '';
const fieldParam = match[2]?.replace('…', '...') || '';
const rawdocid = match[3]?.substring(1);
- const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
- if (!fieldKey && !docid) return state.tr;
- docid &&
- DocServer.GetRefField(docid).then(docx => {
+ const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
+ if (!fieldKey && !docId) return state.tr;
+ docId &&
+ DocServer.GetRefField(docId).then(docx => {
if (!(docx instanceof Doc && docx)) {
- Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid);
+ Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId);
}
});
const node = (state.doc.resolve(start) as any).nodeAfter;
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
const sm = state.storedMarks || undefined;
return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}),
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index aa2475dca..6c9d5d31a 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = {
dashComment: {
attrs: {
- docid: { default: '' },
+ docId: { default: '' },
},
inline: true,
group: 'inline',
@@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = {
title: { default: null },
float: { default: 'left' },
location: { default: 'add:right' },
- docid: { default: '' },
+ docId: { default: '' },
},
group: 'inline',
draggable: true,
@@ -246,7 +246,7 @@ export const nodes: { [index: string]: NodeSpec } = {
float: { default: 'right' },
hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
fieldKey: { default: '' },
- docid: { default: '' },
+ docId: { default: '' },
alias: { default: '' },
},
group: 'inline',
@@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
fieldKey: { default: '' },
- docid: { default: '' },
+ docId: { default: '' },
hideKey: { default: false },
editable: { default: true },
},
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e79e7472a..3589a9065 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField, InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -39,6 +39,7 @@ const { Howl } = require('howler');
export interface pinDataTypes {
scrollable?: boolean;
+ dataviz?: number[];
pannable?: boolean;
viewType?: boolean;
inkable?: boolean;
@@ -59,7 +60,6 @@ export interface PinProps {
hidePresBox?: boolean;
pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
pinDocLayout?: boolean; // pin layout info (width/height/x/y)
- pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
pinAudioPlay?: boolean; // pin audio annotation
pinData?: pinDataTypes;
}
@@ -492,6 +492,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
}
+ if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
+ changed = true;
+ }
if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
if (bestTarget._scrollTop !== activeItem.presViewScroll) {
@@ -919,6 +922,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
@action
enterMinimize = () => {
+ this.updateCurrentPresentation(this.rootDoc);
clearTimeout(this._presTimer);
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
this.props.removeDocument?.(this.layoutDoc);
@@ -939,7 +943,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc._height = 30;
doc._width = PresBox.minimizedWidth;
Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
- PresBox.Instance.initializePresState(PresBox.Instance.itemIndex);
+ PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
return (doc.presStatus = PresStatus.Manual);
}
@@ -1027,8 +1031,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
- isContentActive = (outsideReaction?: boolean) =>
- Doc.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
+ isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction);
+ //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
/**
* For sorting the array so that the order is maintained when it is dropped.
@@ -2435,7 +2439,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
<div className="presPanel-button-text">
Slide {this.itemIndex + 1}
- {this.activeItem.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
+ {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider" />
<div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 698a2817e..92696240b 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -97,7 +97,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
<div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}>
<DocumentView
- Document={this.rootDoc}
+ Document={PresBox.targetRenderedDoc(this.rootDoc)}
DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
@@ -312,7 +312,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => {
const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
- PresBox.pinDocView(activeItem, { pinDocContent: true, pinData: PresBox.pinDataTypes(target) }, target);
+ PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target);
};
@computed get recordingIsInOverlay() {
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index b66f294f4..c38a83238 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -139,7 +139,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
{ fireImmediately: true }
);
this._disposer = reaction(
- () => SelectionManager.Views(),
+ () => SelectionManager.Views().slice(),
selected => {
this._showLinkPopup = false;
this.setGPTPopupVis(false);
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3b101a0c6..db6b1f011 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,5 +1,5 @@
import React = require('react');
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
@@ -7,10 +7,10 @@ import { List } from '../../../fields/List';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { LinkFollower } from '../../util/LinkFollower';
import { undoBatch } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { AnchorMenu } from './AnchorMenu';
import './Annotation.scss';
-import { OpenWhere } from '../nodes/DocumentView';
interface IAnnotationProps extends FieldViewProps {
anno: Doc;
@@ -82,7 +82,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
e.stopPropagation();
} else if (e.button === 0) {
e.stopPropagation();
- LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false);
+ LinkFollower.FollowLink(undefined, this.annoTextRegion, false);
}
};
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index d17d0e13c..c2b9947dd 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -7,7 +7,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
@@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager';
import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocFocusOptions, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import { StyleProp } from '../StyleProvider';
@@ -383,7 +383,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
const target = e.target as any;
if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
this._textSelecting = false;
- document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
} else {
// if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
setTimeout(
@@ -393,7 +392,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' });
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
};
@@ -403,17 +401,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
this.isAnnotating = false;
this._marqueeing = undefined;
this._textSelecting = true;
- document.removeEventListener('pointermove', this.onSelectMove);
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
-
@action
onSelectEnd = (e: PointerEvent): void => {
this.isAnnotating = false;
clearStyleSheetRules(PDFViewer._annotationStyle);
this.props.select(false);
- document.removeEventListener('pointermove', this.onSelectMove);
document.removeEventListener('pointerup', this.onSelectEnd);
const sel = window.getSelection();
@@ -528,7 +522,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
return this.props.styleProvider?.(doc, props, property);
};
- renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+ renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => (
<div
className="pdfViewerDash-overlay"
style={{
@@ -538,7 +532,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.props.fieldKey + '-annotations'}
@@ -549,6 +547,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
ScreenToLocalTransform={this.overlayTransform}
+ isAnyChildContentActive={returnFalse}
+ isAnnotationOverlayScrollable={true}
dropAction={'alias'}
docFilters={docFilters}
select={emptyFunction}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index fe9b13fbe..8f93f1150 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -117,7 +117,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
makeLink = action((linkTo: Doc) => {
const linkFrom = this.props.linkCreateAnchor?.();
if (linkFrom) {
- const link = DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
+ const link = DocUtils.MakeLink(linkFrom, linkTo, {});
link && this.props.linkCreated?.(link);
}
});
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 168e29dd5..662792ff0 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -26,11 +26,12 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty
import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField';
import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util';
import JSZip = require('jszip');
+import * as JSZipUtils from '../JSZipUtils';
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key);
const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+ return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field));
}
export function toScriptString(field: Field): string {
switch (typeof field) {
@@ -517,20 +518,13 @@ export namespace Doc {
}
export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
if (key.startsWith('_')) key = key.substring(1);
- const hasProto = doc.proto instanceof Doc;
+ const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined;
const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
doc[key] = value;
} else doc.proto![key] = value;
}
- export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
- const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc;
-
- if (proto) {
- proto[key] = value;
- }
- }
export function GetAllPrototypes(doc: Doc): Doc[] {
const protos: Doc[] = [];
let d: Opt<Doc> = doc;
@@ -701,30 +695,43 @@ export namespace Doc {
return bestAlias ?? Doc.MakeAlias(doc);
}
- export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
+ // this lists out all the tag ids that can be in a RichTextField that might contain document ids.
+ // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
+ export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean): Promise<Doc> {
if (Doc.IsBaseProto(doc)) return doc;
if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
- const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) ?? doc : doc) : new Doc(undefined, true);
+ const copy = new Doc(undefined, true);
cloneMap.set(doc[Id], copy);
const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
await Promise.all(
Object.keys(doc).map(async key => {
if (filter.includes(key)) return;
- const assignKey = (val: any) => !dontCreate && (copy[key] = val);
+ const assignKey = (val: any) => (copy[key] = val);
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
const copyObjectField = async (field: ObjectField) => {
const list = await Cast(doc[key], listSpec(Doc));
const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
if (docs !== undefined && docs.length) {
- const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
- !dontCreate && assignKey(new List<Doc>(clones));
- } else if (doc[key] instanceof Doc) {
- assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
+ const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks)));
+ assignKey(new List<Doc>(clones));
} else {
- !dontCreate && assignKey(ObjectField.MakeCopy(field));
+ assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
- if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+ if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
+ const rawdocids = field.Data.match(docidsearch);
+ const docids = rawdocids?.map((str: string) =>
+ DocsInTextFieldIds.reduce((output, exp) => {
+ return output.replace(new RegExp(`${exp}":`, 'g'), '');
+ }, str)
+ .replace(/"/g, '')
+ .trim()
+ );
+ const results = docids && (await DocServer.GetRefFields(docids));
+ const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
+ docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
rtfs.push({ copy, key, field });
}
}
@@ -732,64 +739,71 @@ export namespace Doc {
};
const docAtKey = doc[key];
if (docAtKey instanceof Doc) {
- if (!Doc.IsSystem(docAtKey) && (key === 'annotationOn' || (key === 'proto' && cloneMap.has(doc[Id])) || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
- assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
+ if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+ assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
} else {
assignKey(docAtKey);
}
+ } else if (field instanceof RefField) {
+ assignKey(field);
+ } else if (cfield instanceof ComputedField) {
+ assignKey(cfield[Copy]());
+ // ComputedField.MakeFunction(cfield.script.originalScript));
+ } else if (field instanceof ObjectField) {
+ await copyObjectField(field);
+ } else if (field instanceof Promise) {
+ debugger; //This shouldn't happen...
} else {
- if (field instanceof RefField) {
- assignKey(field);
- } else if (cfield instanceof ComputedField) {
- !dontCreate && assignKey(cfield[Copy]());
- // ComputedField.MakeFunction(cfield.script.originalScript));
- } else if (field instanceof ObjectField) {
- await copyObjectField(field);
- } else if (field instanceof Promise) {
- debugger; //This shouldn't happen...
- } else {
- assignKey(field);
- }
+ assignKey(field);
}
})
);
- for (const link of Array.from(doc[DirectLinksSym])) {
- const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
- linkMap.set(link[Id], linkClone);
- }
- if (!dontCreate) {
- Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
- asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
- if (!Doc.IsPrototype(copy)) {
- Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy));
+ Array.from(doc[DirectLinksSym]).forEach(async link => {
+ if (
+ cloneLinks ||
+ ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id])))
+ ) {
+ linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, cloneLinks));
}
- cloneMap.set(doc[Id], copy);
- }
+ });
+ Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true);
+ copy.cloneOf = doc;
+ cloneMap.set(doc[Id], copy);
+
Doc.AddFileOrphan(copy);
return copy;
}
- export function repairClone(doc: Doc, cloned: Doc[], visited: Set<Doc>) {
- if (visited.has(doc)) return;
- visited.add(doc);
- Object.keys(doc).map(key => {
- const docAtKey = DocCast(doc[key]);
- if (docAtKey && !Doc.IsSystem(docAtKey)) {
- if (!cloned.includes(docAtKey)) {
- doc[key] = undefined;
- } else {
- repairClone(docAtKey, cloned, visited);
+ export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
+ if (visited.has(clone)) return;
+ visited.add(clone);
+ Object.keys(clone)
+ .filter(key => key !== 'cloneOf')
+ .map(key => {
+ const docAtKey = DocCast(clone[key]);
+ if (docAtKey && !Doc.IsSystem(docAtKey)) {
+ if (!Array.from(cloneMap.values()).includes(docAtKey)) {
+ if (cloneMap.has(docAtKey[Id])) {
+ clone[key] = cloneMap.get(docAtKey[Id]);
+ } else clone[key] = undefined;
+ } else {
+ repairClone(docAtKey, cloneMap, visited);
+ }
}
- }
- });
+ });
+ }
+ export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+ const cloneMap = new Map<string, Doc>();
+ return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
}
- export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
+
+ export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
const linkMap = new Map<string, Doc>();
const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf'], dontCreate, asBranch);
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], cloneLinks);
const repaired = new Set<Doc>();
const linkedDocs = Array.from(linkMap.values());
const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
- clonedDocs.map(clone => Doc.repairClone(clone, Array.from(cloneMap.values()), repaired));
+ clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired));
linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
@@ -802,7 +816,8 @@ export namespace Doc {
};
const regex = `(${Doc.localServerPath()})([^"]*)`;
const re = new RegExp(regex, 'g');
- copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g');
+ copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
});
return { clone: copy, map: cloneMap, linkMap };
}
@@ -813,10 +828,11 @@ export namespace Doc {
// a.href = url;
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
- const { clone, map, linkMap } = await Doc.MakeClone(doc, true);
+ const { clone, map, linkMap } = await Doc.MakeClone(doc);
clone.LINKS = new List<Doc>(Array.from(linkMap.values()));
+ const proms = [] as string[];
function replacer(key: any, value: any) {
- if (['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
+ if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
else if (value instanceof Doc) {
if (key !== 'field' && Number.isNaN(Number(key))) {
const __fields = value[FieldsSym]();
@@ -826,9 +842,14 @@ export namespace Doc {
}
} else if (value instanceof ScriptField) return { script: value.script, __type: 'script' };
else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: 'RichTextField' };
- else if (value instanceof ImageField) return { url: value.url.href, __type: 'image' };
- else if (value instanceof PdfField) return { url: value.url.href, __type: 'pdf' };
- else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' };
+ else if (value instanceof ImageField) {
+ const extension = value.url.href.replace(/.*\./, '');
+ proms.push(value.url.href.replace('.' + extension, '_o.' + extension));
+ return { url: value.url.href, __type: 'image' };
+ } else if (value instanceof PdfField) {
+ proms.push(value.url.href);
+ return { url: value.url.href, __type: 'pdf' };
+ } else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' };
else if (value instanceof VideoField) return { url: value.url.href, __type: 'video' };
else if (value instanceof WebField) return { url: value.url.href, __type: 'web' };
else if (value instanceof MapField) return { url: value.url.href, __type: 'map' };
@@ -841,8 +862,34 @@ export namespace Doc {
const docs: { [id: string]: any } = {};
Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
- const docString = JSON.stringify({ id: doc[Id], docs }, decycle(replacer));
-
+ const docString = JSON.stringify({ id: clone[Id], docs }, decycle(replacer));
+
+ let generateZIP = (proms: string[]) => {
+ var zip = new JSZip();
+ var count = 0;
+ var zipFilename = 'dashExport.zip';
+
+ proms
+ .filter(url => url.startsWith(window.location.origin))
+ .forEach((url, i) => {
+ var filename = proms[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
+ // loading a file and add it in a zip file
+ JSZipUtils.getBinaryContent(url, function (err: any, data: any) {
+ if (err) {
+ throw err; // or handle the error
+ }
+ zip.file(filename, data, { binary: true });
+ count++;
+ if (count == proms.length) {
+ zip.file('doc.json', docString);
+ zip.generateAsync({ type: 'blob' }).then(function (content) {
+ saveAs(content, zipFilename);
+ });
+ }
+ });
+ });
+ };
+ generateZIP(proms);
const zip = new JSZip();
zip.file('doc.json', docString);
@@ -859,57 +906,40 @@ export namespace Doc {
saveAs(content, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title?
});
}
- //
- // Determines whether the layout needs to be expanded (as a template).
- // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
- //
- export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc;
- }
const _pendingMap: Map<string, boolean> = new Map();
//
// Returns an expanded template layout for a target data document if there is a template relationship
// between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
// of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
- // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
- // NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
- // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
- // the derefence will then occur on the rootDocument (the original document).
- // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
- // layout_mytemplate(somparam=somearg).
- // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
- export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
- const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace('()', '') || StrCast(templateLayoutDoc.PARAMS);
- if ((!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc)) || !targetDoc) return templateLayoutDoc;
-
- const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders
+ export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) {
+ // nothing to do if the layout isn't a template or we don't have a target that's different than the template
+ if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) {
+ return templateLayoutDoc;
+ }
+
+ const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutFieldKey(templateLayoutDoc)); // the field that the template renders
// First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
// using the template layout doc's id as the field key.
// If it doesn't find the expanded layout, then it makes a delegate of the template layout and
// saves it on the data doc indexed by the template layout's id.
//
- const params = args.split('=').length > 1 ? args.split('=')[0] : 'PARAMS';
- const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
- const expandedLayoutFieldKey = (templateField || layoutFielddKey) + '-layout[' + templateLayoutDoc[Id] + (args ? `(${args})` : '') + ']';
+ const expandedLayoutFieldKey = templateField + '-layout[' + templateLayoutDoc[Id] + ']';
let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
expandedTemplateLayout = undefined;
_pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
- } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) {
- if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) {
+ } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey)) {
+ if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc))) {
expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
} else {
templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
if (!targetDoc[expandedLayoutFieldKey]) {
- _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey + args, true);
+ _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
setTimeout(
action(() => {
const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, '[' + templateLayoutDoc.title + ']');
- // the template's arguments are stored in params which is derefenced to find
- // the actual field key where the parameterized template data is stored.
- newLayoutDoc[params] = args !== '...' ? args : ''; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
newLayoutDoc.rootDocument = targetDoc;
const dataDoc = Doc.GetProto(targetDoc);
newLayoutDoc.resolvedDataDoc = dataDoc;
@@ -918,7 +948,7 @@ export namespace Doc {
}
targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
- _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args);
+ _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey);
})
);
}
@@ -934,8 +964,8 @@ export namespace Doc {
console.log('No, no, no!');
return { layout: childDoc, data: childDoc };
}
- const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc;
- return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, '(' + StrCast(containerDoc.PARAMS) + ')'), data: resolvedDataDoc };
+ const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc;
+ return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
}
export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
@@ -1166,7 +1196,7 @@ export namespace Doc {
return doc[StrCast(doc.layoutKey, 'layout')];
}
export function LayoutFieldKey(doc: Doc): string {
- return StrCast(Doc.Layout(doc).layout).split("'")[1];
+ return StrCast(Doc.Layout(doc)[StrCast(doc.layoutKey, 'layout')]).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layoutKey
}
export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
@@ -1175,9 +1205,10 @@ export namespace Doc {
return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeWidth'], useWidth ? doc[WidthSym]() : 0));
}
export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
- const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0;
- const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0;
- return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+ if (!doc) return 0;
+ const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]();
+ const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0);
+ return NumCast(doc._nativeHeight, nheight || dheight);
}
export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width;
@@ -1287,6 +1318,7 @@ export namespace Doc {
}
export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
+ if (linkDoc.anchor2 === anchorDoc || (linkDoc.anchor2 as Doc).annotationOn) return '2';
return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2';
}
@@ -1345,7 +1377,7 @@ export namespace Doc {
});
}
export function UnBrushAllDocs() {
- brushManager.BrushedDoc.clear();
+ runInAction(() => brushManager.BrushedDoc.clear());
}
export function getDocTemplate(doc?: Doc) {
@@ -1527,9 +1559,12 @@ export namespace Doc {
formData.append('remap', 'true');
const response = await fetch(upload, { method: 'POST', body: formData });
const json = await response.json();
+ console.log(json);
if (json !== 'error') {
- const doc = DocCast(await DocServer.GetRefField(json));
+ await DocServer.GetRefFields(json.docids as string[]);
+ const doc = DocCast(await DocServer.GetRefField(json.id));
(await DocListCastAsync(doc?.LINKS))?.forEach(link => LinkManager.Instance.addLink(link));
+ doc.LINKS = undefined;
return doc;
}
}
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index d7edd4266..3e75a071f 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,11 +1,11 @@
-import { serializable } from "serializr";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ObjectField } from "./ObjectField";
+import { serializable } from 'serializr';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
@scriptingGlobal
-@Deserializable("RichTextField")
+@Deserializable('RichTextField')
export class RichTextField extends ObjectField {
@serializable(true)
readonly Data: string;
@@ -13,14 +13,14 @@ export class RichTextField extends ObjectField {
@serializable(true)
readonly Text: string;
- constructor(data: string, text: string = "") {
+ constructor(data: string, text: string = '') {
super();
this.Data = data;
this.Text = text;
}
Empty() {
- return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align"));
+ return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align'));
}
[Copy]() {
@@ -28,14 +28,16 @@ export class RichTextField extends ObjectField {
}
[ToScriptString]() {
- return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`;
+ return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`;
}
[ToString]() {
return this.Text;
}
public static DashField(fieldKey: string) {
- return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, "");
+ return new RichTextField(
+ `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`,
+ ''
+ );
}
-
-} \ No newline at end of file
+}
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index bf055cd8b..239b59e83 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -264,18 +264,18 @@ export namespace RichTextUtils {
const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
const { url: src, width, agnostic } = image;
- let docid: string;
+ let docId: string;
const guid = Utils.GenerateDeterministicGuid(agnostic);
const backingDocId = StrCast(textNote[guid]);
if (!backingDocId) {
const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
- docid = backingDoc[Id];
- textNote[guid] = docid;
+ docId = backingDoc[Id];
+ textNote[guid] = docId;
} else {
- docid = backingDocId;
+ docId = backingDocId;
}
- return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' });
+ return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' });
};
const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index feb419597..2b8750714 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -114,7 +114,7 @@ export class ScriptField extends ObjectField {
}
[ToScriptString]() {
- return 'script field';
+ return this.script.originalScript;
}
[ToString]() {
return this.script.originalScript;
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 5b489a96c..b7fd06973 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -89,7 +89,6 @@ export const documentSchema = createSchema({
hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations
- isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
layers: listSpec('string'), // which layers the document is part of
_lockedPosition: 'boolean', // whether the document can be moved (dragged)
_lockedTransform: 'boolean', // whether a freeformview can pan/zoom
diff --git a/src/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts
new file mode 100644
index 000000000..0d43130d1
--- /dev/null
+++ b/src/server/ApiManagers/DataVizManager.ts
@@ -0,0 +1,26 @@
+import { csvParser, csvToString } from "../DataVizUtils";
+import { Method, _success } from "../RouteManager";
+import ApiManager, { Registration } from "./ApiManager";
+import { Directory, serverPathToFile } from "./UploadManager";
+import * as path from 'path';
+
+export default class DataVizManager extends ApiManager {
+ protected initialize(register: Registration): void {
+ register({
+ method: Method.GET,
+ subscription: "/csvData",
+ secureHandler: async ({ req, res }) => {
+ const uri = req.query.uri as string;
+
+ return new Promise<void>(resolve => {
+ const name = path.basename(uri);
+ const sPath = serverPathToFile(Directory.csv, name);
+ const parsedCsv = csvParser(csvToString(sPath));
+ _success(res, parsedCsv);
+ resolve();
+ });
+ }
+ });
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 9bacbd5c8..6e28268a9 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -228,24 +228,29 @@ export default class UploadManager extends ApiManager {
form.parse(req, async (_err, fields, files) => {
remap = fields.remap !== 'false';
let id: string = '';
+ let docids: string[] = [];
try {
for (const name in files) {
const f = files[name];
const path_2 = Array.isArray(f) ? '' : f.path;
const zip = new AdmZip(path_2);
zip.getEntries().forEach((entry: any) => {
- if (!entry.entryName.startsWith('files/')) return;
- let directory = dirname(entry.entryName) + '/';
- const extension = extname(entry.entryName);
- const base = basename(entry.entryName).split('.')[0];
+ let entryName = entry.entryName.replace(/%%%/g, '/');
+ if (!entryName.startsWith('files/')) {
+ return;
+ }
+ const extension = extname(entryName);
+ const pathname = publicDirectory + '/' + entry.entryName;
+ const targetname = publicDirectory + '/' + entryName;
try {
zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
- directory = '/' + directory;
-
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_o' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_s' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_m' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_l' + extension));
+ createReadStream(pathname).pipe(createWriteStream(targetname));
+ if (extension !== '.pdf') {
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_s' + extension)));
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_m' + extension)));
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_l' + extension)));
+ }
+ unlink(pathname, () => {});
} catch (e) {
console.log(e);
}
@@ -257,6 +262,7 @@ export default class UploadManager extends ApiManager {
id = getId(data.id);
const docs = Object.keys(datadocs).map(key => datadocs[key]);
docs.forEach(mapFn);
+ docids = docs.map(doc => doc.id);
await Promise.all(
docs.map(
(doc: any) =>
@@ -279,7 +285,7 @@ export default class UploadManager extends ApiManager {
unlink(path_2, () => {});
}
SolrManager.update();
- res.send(JSON.stringify(id || 'error'));
+ res.send(JSON.stringify({ id, docids } || 'error'));
} catch (e) {
console.log(e);
}
diff --git a/src/server/DataVizUtils.ts b/src/server/DataVizUtils.ts
index 4fd0ca6ff..15f03b319 100644
--- a/src/server/DataVizUtils.ts
+++ b/src/server/DataVizUtils.ts
@@ -1,13 +1,17 @@
+import { readFileSync } from 'fs';
+
export function csvParser(csv: string) {
- const lines = csv.split("\n");
- const headers = lines[0].split(",");
- const data = lines.slice(1).map(line => {
- const values = line.split(",");
- const obj: any = {};
- for (let i = 0; i < headers.length; i++) {
- obj[headers[i]] = values[i];
- }
- return obj;
- });
+ const lines = csv.split('\n');
+ const headers = lines[0].split(',').map(header => header.trim());
+ const data = lines.slice(1).map(line =>
+ line.split(',').reduce((last, value, i) => {
+ last[headers[i]] = value.trim();
+ return last;
+ }, {} as any)
+ );
return data;
-} \ No newline at end of file
+}
+
+export function csvToString(path: string) {
+ return readFileSync(path, 'utf8');
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 6e6bde3cb..6562860fe 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,6 +4,7 @@ import * as mobileDetect from 'mobile-detect';
import * as path from 'path';
import * as qs from 'query-string';
import { log_execution } from './ActionUtilities';
+import DataVizManager from './ApiManagers/DataVizManager';
import DeleteManager from './ApiManagers/DeleteManager';
import DownloadManager from './ApiManagers/DownloadManager';
import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager';
@@ -62,7 +63,19 @@ async function preliminaryFunctions() {
* with the server
*/
function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) {
- const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new PDFManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), new GooglePhotosManager()];
+ const managers = [
+ new SessionManager(),
+ new UserManager(),
+ new UploadManager(),
+ new DownloadManager(),
+ new SearchManager(),
+ new PDFManager(),
+ new DeleteManager(),
+ new UtilManager(),
+ new GeneralGoogleManager(),
+ new GooglePhotosManager(),
+ new DataVizManager(),
+ ];
// initialize API Managers
console.log(yellow('\nregistering server routes...'));