aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts7
-rw-r--r--src/client/DocServer.ts30
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts4
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx4
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts22
-rw-r--r--src/client/documents/Documents.ts193
-rw-r--r--src/client/northstar/dash-fields/HistogramField.ts5
-rw-r--r--src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts4
-rw-r--r--src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx4
-rw-r--r--src/client/util/DictationManager.ts7
-rw-r--r--src/client/util/DragManager.ts6
-rw-r--r--src/client/util/DropConverter.ts40
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx8
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts12
-rw-r--r--src/client/util/InteractionUtils.tsx (renamed from src/client/util/InteractionUtils.ts)19
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts2
-rw-r--r--src/client/util/RichTextMenu.scss121
-rw-r--r--src/client/util/RichTextMenu.tsx860
-rw-r--r--src/client/util/RichTextRules.ts112
-rw-r--r--src/client/util/RichTextSchema.tsx201
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SettingsManager.scss136
-rw-r--r--src/client/util/SettingsManager.tsx131
-rw-r--r--src/client/util/TooltipLinkingMenu.tsx68
-rw-r--r--src/client/util/TooltipTextMenu.scss372
-rw-r--r--src/client/util/TooltipTextMenu.tsx1042
-rw-r--r--src/client/util/request-image-size.js2
-rw-r--r--src/client/util/type_decls.d1
-rw-r--r--src/client/views/AntimodeMenu.scss15
-rw-r--r--src/client/views/AntimodeMenu.tsx34
-rw-r--r--src/client/views/ContextMenu.tsx16
-rw-r--r--src/client/views/DocComponent.tsx18
-rw-r--r--src/client/views/DocumentButtonBar.tsx106
-rw-r--r--src/client/views/DocumentDecorations.tsx162
-rw-r--r--src/client/views/EditableView.tsx25
-rw-r--r--src/client/views/GestureOverlay.scss41
-rw-r--r--src/client/views/GestureOverlay.tsx425
-rw-r--r--src/client/views/InkingControl.tsx62
-rw-r--r--src/client/views/InkingStroke.tsx51
-rw-r--r--src/client/views/MainView.scss20
-rw-r--r--src/client/views/MainView.tsx43
-rw-r--r--src/client/views/MetadataEntryMenu.scss4
-rw-r--r--src/client/views/MetadataEntryMenu.tsx7
-rw-r--r--src/client/views/OCRUtils.ts7
-rw-r--r--src/client/views/OverlayView.tsx1
-rw-r--r--src/client/views/Palette.tsx1
-rw-r--r--src/client/views/PreviewCursor.tsx14
-rw-r--r--src/client/views/ScriptBox.tsx13
-rw-r--r--src/client/views/TemplateMenu.scss13
-rw-r--r--src/client/views/TemplateMenu.tsx111
-rw-r--r--src/client/views/Templates.tsx13
-rw-r--r--src/client/views/TouchScrollableMenu.tsx59
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss40
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx82
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx94
-rw-r--r--src/client/views/collections/CollectionLinearView.scss (renamed from src/client/views/CollectionLinearView.scss)4
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx (renamed from src/client/views/CollectionLinearView.tsx)62
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx4
-rw-r--r--src/client/views/collections/CollectionPivotView.scss88
-rw-r--r--src/client/views/collections/CollectionPivotView.tsx148
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx5
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx3
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx23
-rw-r--r--src/client/views/collections/CollectionStackingView.scss2
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx84
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx68
-rw-r--r--src/client/views/collections/CollectionStaffView.tsx4
-rw-r--r--src/client/views/collections/CollectionSubView.tsx99
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx177
-rw-r--r--src/client/views/collections/CollectionView.tsx110
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss38
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx125
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss2
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx33
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx34
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx265
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx93
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss33
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx258
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx116
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx56
-rw-r--r--src/client/views/linking/LinkEditor.tsx2
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx48
-rw-r--r--src/client/views/linking/LinkMenu.tsx2
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx4
-rw-r--r--src/client/views/nodes/AudioBox.tsx103
-rw-r--r--src/client/views/nodes/ButtonBox.tsx7
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx72
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx24
-rw-r--r--src/client/views/nodes/DocuLinkBox.tsx8
-rw-r--r--src/client/views/nodes/DocumentBox.tsx3
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx11
-rw-r--r--src/client/views/nodes/DocumentView.scss13
-rw-r--r--src/client/views/nodes/DocumentView.tsx287
-rw-r--r--src/client/views/nodes/FieldView.tsx15
-rw-r--r--src/client/views/nodes/FontIconBox.tsx19
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx178
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx4
-rw-r--r--src/client/views/nodes/IconBox.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.scss24
-rw-r--r--src/client/views/nodes/ImageBox.tsx234
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx10
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx3
-rw-r--r--src/client/views/nodes/PDFBox.tsx26
-rw-r--r--src/client/views/nodes/PresBox.tsx8
-rw-r--r--src/client/views/nodes/VideoBox.tsx35
-rw-r--r--src/client/views/nodes/WebBox.tsx22
-rw-r--r--src/client/views/pdf/Annotation.tsx10
-rw-r--r--src/client/views/pdf/PDFMenu.tsx2
-rw-r--r--src/client/views/pdf/PDFViewer.tsx72
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx12
-rw-r--r--src/client/views/search/SearchBox.scss1
-rw-r--r--src/client/views/search/SearchBox.tsx19
-rw-r--r--src/client/views/search/SearchItem.tsx14
-rw-r--r--src/mobile/ImageUpload.tsx4
-rw-r--r--src/mobile/MobileInterface.tsx1
-rw-r--r--src/new_fields/CursorField.ts5
-rw-r--r--src/new_fields/DateField.ts5
-rw-r--r--src/new_fields/Doc.ts282
-rw-r--r--src/new_fields/FieldSymbols.ts3
-rw-r--r--src/new_fields/HtmlField.ts5
-rw-r--r--src/new_fields/IconField.ts5
-rw-r--r--src/new_fields/InkField.ts6
-rw-r--r--src/new_fields/List.ts5
-rw-r--r--src/new_fields/ObjectField.ts5
-rw-r--r--src/new_fields/Proxy.ts5
-rw-r--r--src/new_fields/RefField.ts3
-rw-r--r--src/new_fields/RichTextField.ts5
-rw-r--r--src/new_fields/RichTextUtils.ts6
-rw-r--r--src/new_fields/SchemaHeaderField.ts5
-rw-r--r--src/new_fields/ScriptField.ts18
-rw-r--r--src/new_fields/URLField.ts5
-rw-r--r--src/new_fields/documentSchemas.ts34
-rw-r--r--src/new_fields/util.ts37
-rw-r--r--src/pen-gestures/GestureUtils.ts11
-rw-r--r--src/pen-gestures/ndollar.ts179
-rw-r--r--src/scraping/buxton/node_scraper.ts57
-rw-r--r--src/scraping/buxton/scraper.py104
-rw-r--r--src/scraping/buxton/source/Bill_Notes_NB75D.docxbin0 -> 27696302 bytes
-rw-r--r--src/server/ActionUtilities.ts26
-rw-r--r--src/server/ApiManagers/DeleteManager.ts17
-rw-r--r--src/server/ApiManagers/SearchManager.ts16
-rw-r--r--src/server/ApiManagers/SessionManager.ts58
-rw-r--r--src/server/ApiManagers/UploadManager.ts17
-rw-r--r--src/server/ApiManagers/UserManager.ts55
-rw-r--r--src/server/DashSession.ts62
-rw-r--r--src/server/DashSession/DashSessionAgent.ts223
-rw-r--r--src/server/DashSession/templates/crash_instructions.txt14
-rw-r--r--src/server/DashSession/templates/remote_debug_instructions.txt16
-rw-r--r--src/server/DashUploadUtils.ts96
-rw-r--r--src/server/IDatabase.ts24
-rw-r--r--src/server/MemoryDatabase.ts100
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/RouteManager.ts50
-rw-r--r--src/server/Search.ts8
-rw-r--r--src/server/Session/session.ts592
-rw-r--r--src/server/Session/session_config_schema.ts39
-rw-r--r--src/server/Websocket/Websocket.ts37
-rw-r--r--src/server/authentication/models/current_user_utils.ts95
-rw-r--r--src/server/database.ts19
-rw-r--r--src/server/index.ts45
-rw-r--r--src/server/repl.ts123
-rw-r--r--src/server/server_Initialization.ts4
-rw-r--r--src/typings/index.d.ts2
166 files changed, 6099 insertions, 4579 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 04fe6750b..4deac9035 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -48,7 +48,7 @@ export namespace Utils {
}
export async function getApiKey(target: string): Promise<string> {
- const response = await fetch(prepend(`environment/${target.toUpperCase()}`));
+ const response = await fetch(prepend(`/environment/${target.toUpperCase()}`));
return response.text();
}
@@ -328,8 +328,8 @@ export function timenow() {
return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
}
-export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
- return boundsList.reduce((bounds, b) => {
+export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[], xpad: number, ypad: number) {
+ const bounds = boundsList.reduce((bounds, b) => {
const [sptX, sptY] = [b.x, b.y];
const [bptX, bptY] = [sptX + b.width, sptY + b.height];
return {
@@ -337,6 +337,7 @@ export function aggregateBounds(boundsList: { x: number, y: number, width: numbe
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+ return { x: bounds.x !== Number.MAX_VALUE ? bounds.x - xpad : bounds.x, y: bounds.y !== Number.MAX_VALUE ? bounds.y - ypad : bounds.y, r: bounds.r !== -Number.MAX_VALUE ? bounds.r + xpad : bounds.r, b: bounds.b !== -Number.MAX_VALUE ? bounds.b + ypad : bounds.b };
}
export function intersectRect(r1: { left: number, top: number, width: number, height: number },
r2: { left: number, top: number, width: number, height: number }) {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index ed7fbd7ba..5fcd2547e 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -64,6 +64,24 @@ export namespace DocServer {
}
}
+ const instructions = "This page will automatically refresh after this alert is closed. Expect to reconnect after about 30 seconds.";
+ function alertUser(connectionTerminationReason: string) {
+ switch (connectionTerminationReason) {
+ case "crash":
+ alert(`Dash has temporarily crashed. Administrators have been notified and the server is restarting itself. ${instructions}`);
+ break;
+ case "temporary":
+ alert(`An administrator has chosen to restart the server. ${instructions}`);
+ break;
+ case "exit":
+ alert("An administrator has chosen to kill the server. Do not expect to reconnect until administrators start the server.");
+ break;
+ default:
+ console.log(`Received an unknown ConnectionTerminated message: ${connectionTerminationReason}`);
+ }
+ window.location.reload();
+ }
+
export function init(protocol: string, hostname: string, port: number, identifier: string) {
_cache = {};
GUID = identifier;
@@ -82,9 +100,7 @@ export namespace DocServer {
Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate);
Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
- Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, () => {
- alert("Your connection to the server has been terminated.");
- });
+ Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
}
function errorFunc(): never {
@@ -202,6 +218,10 @@ export namespace DocServer {
return apiKey;
}
+ export async function analyzeImage(image: string, callback: (result: any) => void) {
+ Utils.EmitCallback(_socket, MessageStore.AnalyzeInk, image, callback);
+ }
+
export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
}
@@ -256,7 +276,7 @@ export namespace DocServer {
const fieldMap: { [id: string]: RefField } = {};
const proms: Promise<void>[] = [];
for (const field of fields) {
- if (field !== undefined) {
+ if (field !== undefined && field !== null) {
// deserialize
const prom = SerializationHelper.Deserialize(field).then(deserialized => {
fieldMap[field.id] = deserialized;
@@ -423,4 +443,4 @@ export namespace DocServer {
function respondToDelete(ids: string | string[]) {
_respondToDelete(ids);
}
-} \ No newline at end of file
+}
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 966d8053a..7e5d5fe1b 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -136,7 +136,7 @@ export namespace GooglePhotos {
document.contentSize = upload.contentSize;
return document;
});
- const options = { width: 500, height: 500 };
+ const options = { _width: 500, _height: 500 };
return constructor(children, options);
};
@@ -340,7 +340,7 @@ export namespace GooglePhotos {
const url = data.url.href;
const target = Doc.MakeAlias(source);
const description = parseDescription(target, descriptionKey);
- await DocumentView.makeCustomViewClicked(target, undefined);
+ await DocumentView.makeCustomViewClicked(target, undefined, Docs.Create.FreeformDocument);
media.push({ url, description });
}
if (media.length) {
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index fd3d9e2f1..c940bba43 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -46,7 +46,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
* When component mounts, last search's results are laoded in based on the back up stored
* in the document of the props.
*/
- async componentWillMount() {
+ async componentDidMount() {
//DocServer.getYoutubeChannels();
const castedSearchBackUp = Cast(this.props.Document.cachedSearchResults, Doc);
const awaitedBackUp = await castedSearchBackUp;
@@ -337,7 +337,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
const newVideoX = NumCast(this.props.Document.x);
const newVideoY = NumCast(this.props.Document.y) + NumCast(this.props.Document.height);
- addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, width: 400, height: 315, x: newVideoX, y: newVideoY }));
+ addFunction(Docs.Create.VideoDocument(embeddedUrl, { title: filteredTitle, _width: 400, _height: 315, x: newVideoX, y: newVideoY }));
this.videoClicked = true;
}
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 02eff3b25..62308f056 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -47,7 +47,8 @@ export namespace CognitiveServices {
let results: any;
try {
results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
- } catch {
+ } catch (e) {
+ throw e;
results = undefined;
}
return results;
@@ -137,7 +138,7 @@ export namespace CognitiveServices {
let id = 0;
const strokes: AzureStrokeData[] = inkData.map(points => ({
id: id++,
- points: points.map(({ x, y }) => `${x},${y}`).join(","),
+ points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(","),
language: "en-US"
}));
return JSON.stringify({
@@ -153,7 +154,7 @@ export namespace CognitiveServices {
const serverAddress = "https://api.cognitive.microsoft.com";
const endpoint = serverAddress + "/inkrecognizer/v1.0-preview/recognize";
- const promisified = (resolve: any, reject: any) => {
+ return new Promise<string>((resolve, reject) => {
xhttp.onreadystatechange = function () {
if (this.readyState === 4) {
const result = xhttp.responseText;
@@ -171,11 +172,8 @@ export namespace CognitiveServices {
xhttp.setRequestHeader('Ocp-Apim-Subscription-Key', apiKey);
xhttp.setRequestHeader('Content-Type', 'application/json');
xhttp.send(body);
- };
-
- return new Promise<any>(promisified);
+ });
},
-
};
export namespace Appliers {
@@ -188,13 +186,21 @@ export namespace CognitiveServices {
results.recognitionUnits && (results = results.recognitionUnits);
target[keys[0]] = Docs.Get.DocumentHierarchyFromJson(results, "Ink Analysis");
const recognizedText = results.map((item: any) => item.recognizedText);
+ const recognizedObjects = results.map((item: any) => item.recognizedObject);
const individualWords = recognizedText.filter((text: string) => text && text.split(" ").length === 1);
- target[keys[1]] = individualWords.join(" ");
+ target[keys[1]] = individualWords.length ? individualWords.join(" ") : recognizedObjects.join(", ");
}
batch.end();
};
+ export const InterpretStrokes = async (strokes: InkData[]) => {
+ let results = await ExecuteQuery(Service.Handwriting, Manager, strokes);
+ if (results) {
+ results.recognitionUnits && (results = results.recognitionUnits);
+ }
+ return results;
+ }
}
export interface AzureStrokeData {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64abd4f57..1c13eb079 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,7 +19,7 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
import { OmitKeys, JSONUtils } from "../../Utils";
-import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
+import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../new_fields/Doc";
import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
@@ -51,40 +51,53 @@ import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
import { DocumentBox } from "../views/nodes/DocumentBox";
import { InkingStroke } from "../views/InkingStroke";
import { InkField } from "../../new_fields/InkField";
+import { InkingControl } from "../views/InkingControl";
+import { RichTextField } from "../../new_fields/RichTextField";
const requestImageSize = require('../util/request-image-size');
const path = require('path');
export interface DocumentOptions {
+ _autoHeight?: boolean;
+ _panX?: number;
+ _panY?: number;
+ _width?: number;
+ _height?: number;
+ _nativeWidth?: number;
+ _nativeHeight?: number;
+ _fitWidth?: boolean;
+ _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+ _LODdisable?: boolean;
+ _dropAction?: dropActionType;
+ _chromeStatus?: string;
+ _viewType?: number;
+ _gridGap?: number; // gap between items in masonry view
+ _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
+ _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
+ _textTemplate?: RichTextField; // template used by a formattedTextBox to create a text box to render
+ _itemIndex?: number; // which item index the carousel viewer is showing
+ _hideSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
x?: number;
y?: number;
z?: number;
+ layoutKey?: string;
type?: string;
- width?: number;
- height?: number;
- nativeWidth?: number;
- nativeHeight?: number;
title?: string;
- panX?: number;
- panY?: number;
page?: number;
scale?: number;
- fitWidth?: boolean;
+ isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
forceActive?: boolean;
preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
layout?: string | Doc;
hideHeadings?: boolean; // whether stacking view column headings should be hidden
- isTemplateField?: boolean;
+ isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: boolean;
templates?: List<string>;
- viewType?: number;
- backgroundColor?: string;
+ backgroundColor?: string | ScriptField;
ignoreClick?: boolean;
lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
opacity?: number;
defaultBackgroundColor?: string;
- dropAction?: dropActionType;
- chromeStatus?: string;
columnWidth?: number;
fontSize?: number;
curPage?: number;
@@ -96,29 +109,36 @@ export interface DocumentOptions {
sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
- autoHeight?: boolean;
annotationOn?: Doc;
removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
dbDoc?: Doc;
ischecked?: ScriptField; // returns whether a font icon box is checked
activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
onClick?: ScriptField;
+ onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
+ clipboard?: Doc; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
icon?: string;
- gridGap?: number; // gap between items in masonry view
- xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
- yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
strokeWidth?: number;
color?: string;
+ treeViewHideTitle?: boolean; // whether to hide the title of a tree view
+ treeViewOpen?: boolean; // whether this document is expanded in a tree view
+ treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
+ isFacetFilter?: boolean; // whether document functions as a facet filter in a tree view
limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
+ isExpanded?: boolean; // is linear view expanded
+ textTransform?: string; // is linear view expanded
+ letterSpacing?: string; // is linear view expanded
+ flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse";
+ selectedIndex?: number;
}
class EmptyBox {
@@ -146,19 +166,19 @@ export namespace Docs {
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
layout: { view: FormattedTextBox, dataField: data },
- options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
+ options: { _height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
}],
[DocumentType.HIST, {
layout: { view: HistogramBox, dataField: data },
- options: { height: 300, backgroundColor: "black" }
+ options: { _height: 300, backgroundColor: "black" }
}],
[DocumentType.QUERY, {
layout: { view: QueryBox, dataField: data },
- options: { width: 400 }
+ options: { _width: 400 }
}],
[DocumentType.COLOR, {
layout: { view: ColorBox, dataField: data },
- options: { nativeWidth: 220, nativeHeight: 300 }
+ options: { _nativeWidth: 220, _nativeHeight: 300 }
}],
[DocumentType.IMG, {
layout: { view: ImageBox, dataField: data },
@@ -166,19 +186,19 @@ export namespace Docs {
}],
[DocumentType.WEB, {
layout: { view: WebBox, dataField: data },
- options: { height: 300 }
+ options: { _height: 300 }
}],
[DocumentType.COL, {
layout: { view: CollectionView, dataField: data },
- options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
+ options: { _panX: 0, _panY: 0, scale: 1, _width: 500, _height: 500 }
}],
[DocumentType.KVP, {
layout: { view: KeyValueBox, dataField: data },
- options: { height: 150 }
+ options: { _height: 150 }
}],
[DocumentType.DOCUMENT, {
layout: { view: DocumentBox, dataField: data },
- options: { height: 250 }
+ options: { _height: 250 }
}],
[DocumentType.VID, {
layout: { view: VideoBox, dataField: data },
@@ -186,7 +206,7 @@ export namespace Docs {
}],
[DocumentType.AUDIO, {
layout: { view: AudioBox, dataField: data },
- options: { height: 35, backgroundColor: "lightGray" }
+ options: { _height: 35, backgroundColor: "lightGray" }
}],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: data },
@@ -194,11 +214,11 @@ export namespace Docs {
}],
[DocumentType.ICON, {
layout: { view: IconBox, dataField: data },
- options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
+ options: { _width: Number(MINIMIZED_ICON_SIZE), _height: Number(MINIMIZED_ICON_SIZE) },
}],
[DocumentType.IMPORT, {
layout: { view: DirectoryImportBox, dataField: data },
- options: { height: 150 }
+ options: { _height: 150 }
}],
[DocumentType.LINKDOC, {
data: new List<Doc>(),
@@ -216,7 +236,7 @@ export namespace Docs {
}],
[DocumentType.FONTICON, {
layout: { view: FontIconBox, dataField: data },
- options: { width: 40, height: 40, borderRounding: "100%" },
+ options: { _width: 40, _height: 40, borderRounding: "100%" },
}],
[DocumentType.LINKFOLLOW, {
layout: { view: LinkFollowBox, dataField: data }
@@ -231,7 +251,7 @@ export namespace Docs {
]);
// All document prototypes are initialized with at least these values
- const defaultOptions: DocumentOptions = { x: 0, y: 0, width: 300 };
+ const defaultOptions: DocumentOptions = { x: 0, y: 0, _width: 300 };
const suffix = "Proto";
/**
@@ -309,7 +329,8 @@ export namespace Docs {
// whatever options pertain to this specific prototype
const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
options.layout = layout.view.LayoutString(layout.dataField);
- return Doc.assign(new Doc(prototypeId, true), { ...options });
+ const doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options });
+ return doc;
}
}
@@ -320,7 +341,8 @@ export namespace Docs {
*/
export namespace Create {
- const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "annotationOn", "forceActive", "fitWidth"];
+ const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "_dropAction", "_annotationOn",
+ "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_hideSidebar"];
/**
* This function receives the relevant document prototype and uses
@@ -358,7 +380,7 @@ export namespace Docs {
AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
- return Doc.assign(viewDoc, delegateProps);
+ return Doc.assign(viewDoc, delegateProps, true);
}
/**
@@ -390,11 +412,11 @@ export namespace Docs {
requestImageSize(target)
.then((size: any) => {
const aspect = size.height / size.width;
- if (!inst.nativeWidth) {
- inst.nativeWidth = size.width;
+ if (!inst._nativeWidth) {
+ inst._nativeWidth = size.width;
}
- inst.nativeHeight = NumCast(inst.nativeWidth) * aspect;
- inst.height = NumCast(inst.width) * aspect;
+ inst._nativeHeight = NumCast(inst._nativeWidth) * aspect;
+ inst._height = NumCast(inst._width) * aspect;
})
.catch((err: any) => console.log(err));
// }
@@ -428,8 +450,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options);
}
- export function TextDocument(options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
+ export function TextDocument(text: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options);
}
export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
@@ -453,7 +475,7 @@ export namespace Docs {
const ctlog = await Gateway.Instance.GetSchema(url, schemaName);
if (ctlog && ctlog.schemas) {
const schema = ctlog.schemas[0];
- const schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! });
+ const schemaDoc = Docs.Create.TreeDocument([], { ...options, _nativeWidth: undefined, _nativeHeight: undefined, _width: 150, _height: 100, title: schema.displayName! });
const schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []);
if (!schemaDocuments) {
return;
@@ -470,13 +492,13 @@ export namespace Docs {
new AttributeTransformationModel(atmod, AggregateFunction.None),
new AttributeTransformationModel(atmod, AggregateFunction.Count),
new AttributeTransformationModel(atmod, AggregateFunction.Count));
- docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! }));
+ docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, _width: 200, _height: 200, title: attr.displayName! }));
}
}));
});
return schemaDoc;
}
- return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName });
+ return Docs.Create.TreeDocument([], { _width: 50, _height: 100, title: schemaName });
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
@@ -496,27 +518,35 @@ export namespace Docs {
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Freeform }, id);
}
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Linear }, id);
+ }
+
+ export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Carousel });
}
export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, _viewType: CollectionViewType.Schema });
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Tree });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree });
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Stacking });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking });
+ }
+
+ export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multicolumn });
}
export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Masonry });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Masonry });
}
export function ButtonDocument(options?: DocumentOptions) {
@@ -537,7 +567,7 @@ export namespace Docs {
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
Doc.GetProto(inst).data = new List<Doc>(documents);
return inst;
}
@@ -653,28 +683,70 @@ export namespace Docs {
throw new Error(`How did ${data} of type ${typeof data} end up in JSON?`);
};
+ 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 || {};
+ if (field instanceof ImageField) {
+ created = Docs.Create.ImageDocument((field).url.href, resolved);
+ layout = ImageBox.LayoutString;
+ } else if (field instanceof Doc) {
+ created = field;
+ } else if (field instanceof VideoField) {
+ created = Docs.Create.VideoDocument((field).url.href, resolved);
+ layout = VideoBox.LayoutString;
+ } else if (field instanceof PdfField) {
+ created = Docs.Create.PdfDocument((field).url.href, resolved);
+ layout = PDFBox.LayoutString;
+ } else if (field instanceof IconField) {
+ created = Docs.Create.IconDocument((field).icon, resolved);
+ layout = IconBox.LayoutString;
+ } else if (field instanceof AudioField) {
+ created = Docs.Create.AudioDocument((field).url.href, resolved);
+ layout = AudioBox.LayoutString;
+ } else if (field instanceof HistogramField) {
+ created = Docs.Create.HistogramDocument((field).HistoOp, resolved);
+ layout = HistogramBox.LayoutString;
+ } else if (field instanceof InkField) {
+ const { selectedColor, selectedWidth, selectedTool } = InkingControl.Instance;
+ created = Docs.Create.InkDocument(selectedColor, selectedTool, Number(selectedWidth), (field).inkData, resolved);
+ layout = InkingStroke.LayoutString;
+ } else if (field instanceof List && field[0] instanceof Doc) {
+ created = Docs.Create.StackingDocument(DocListCast(field), resolved);
+ layout = CollectionView.LayoutString;
+ } else {
+ created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
+ layout = FormattedTextBox.LayoutString;
+ }
+ created.layout = layout?.(fieldKey);
+ created.title = fieldKey;
+ proto && (created.proto = Doc.GetProto(proto));
+ return created;
+ }
+
export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
if (type.indexOf("image") !== -1) {
ctor = Docs.Create.ImageDocument;
- if (!options.width) options.width = 300;
+ if (!options._width) options._width = 300;
}
if (type.indexOf("video") !== -1) {
ctor = Docs.Create.VideoDocument;
- if (!options.width) options.width = 600;
- if (!options.height) options.height = options.width * 2 / 3;
+ if (!options._width) options._width = 600;
+ if (!options._height) options._height = options._width * 2 / 3;
}
if (type.indexOf("audio") !== -1) {
ctor = Docs.Create.AudioDocument;
}
if (type.indexOf("pdf") !== -1) {
ctor = Docs.Create.PdfDocument;
- if (!options.width) options.width = 400;
- if (!options.height) options.height = options.width * 1200 / 927;
+ if (!options._width) options._width = 400;
+ if (!options._height) options._height = options._width * 1200 / 927;
}
if (type.indexOf("excel") !== -1) {
ctor = Docs.Create.DBDocument;
- options.dropAction = "copy";
+ options._dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
if (path.includes(window.location.hostname)) {
@@ -685,15 +757,15 @@ export namespace Docs {
const alias = Doc.MakeAlias(field);
alias.x = options.x || 0;
alias.y = options.y || 0;
- alias.width = options.width || 300;
- alias.height = options.height || options.width || 300;
+ alias._width = options._width || 300;
+ alias._height = options._height || options._width || 300;
return alias;
}
return undefined;
});
}
ctor = Docs.Create.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ options = { _height: options._width, ...options, title: path, _nativeWidth: undefined };
}
return ctor ? ctor(path, options) : undefined;
}
@@ -733,7 +805,6 @@ export namespace DocUtils {
});
}
- @undoBatch
export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
@@ -755,8 +826,8 @@ export namespace DocUtils {
linkDocProto.anchor2Groups = new List<Doc>([]);
linkDocProto.anchor1Timecode = source.doc.currentTimecode;
linkDocProto.anchor2Timecode = target.doc.currentTimecode;
- linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
- linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
+ linkDocProto.layout_key1 = DocuLinkBox.LayoutString("anchor1");
+ linkDocProto.layout_key2 = DocuLinkBox.LayoutString("anchor2");
linkDocProto.width = linkDocProto.height = 0;
linkDocProto.isBackground = true;
linkDocProto.isButton = true;
diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts
index f3365e73d..076516977 100644
--- a/src/client/northstar/dash-fields/HistogramField.ts
+++ b/src/client/northstar/dash-fields/HistogramField.ts
@@ -7,7 +7,7 @@ import { ObjectField } from "../../../new_fields/ObjectField";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { OmitKeys } from "../../../Utils";
import { Deserializable } from "../../util/SerializationHelper";
-import { Copy, ToScriptString } from "../../../new_fields/FieldSymbols";
+import { Copy, ToScriptString, ToString } from "../../../new_fields/FieldSymbols";
function serialize(field: HistogramField) {
const obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit;
@@ -60,4 +60,7 @@ export class HistogramField extends ObjectField {
[ToScriptString]() {
return this.toString();
}
+ [ToString]() {
+ return this.toString();
+ }
} \ No newline at end of file
diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
index 3e9145a1b..6b36ffc9e 100644
--- a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
+++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts
@@ -3,7 +3,7 @@ import { AttributeTransformationModel } from "../../northstar/core/attribute/Att
import { ChartType } from '../../northstar/model/binRanges/VisualBinRange';
import { AggregateFunction, Bin, Brush, DoubleValueAggregateResult, HistogramResult, MarginAggregateParameters, MarginAggregateResult } from "../../northstar/model/idea/idea";
import { ModelHelpers } from "../../northstar/model/ModelHelpers";
-import { LABColor } from '../../northstar/utils/LABcolor';
+import { LABColor } from '../../northstar/utils/LABColor';
import { PIXIRectangle } from "../../northstar/utils/MathUtil";
import { StyleConstants } from "../../northstar/utils/StyleContants";
import { HistogramBox } from "./HistogramBox";
@@ -237,4 +237,4 @@ export class HistogramBinPrimitiveCollection {
// }
return StyleConstants.HIGHLIGHT_COLOR;
}
-} \ No newline at end of file
+}
diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
index 5a16b3782..66d91cc1d 100644
--- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx
@@ -5,7 +5,7 @@ import { Utils as DashUtils, emptyFunction } from '../../../Utils';
import { FilterModel } from "../../northstar/core/filter/FilterModel";
import { ModelHelpers } from "../../northstar/model/ModelHelpers";
import { ArrayUtil } from "../../northstar/utils/ArrayUtil";
-import { LABColor } from '../../northstar/utils/LABcolor';
+import { LABColor } from '../../northstar/utils/LABColor';
import { PIXIRectangle } from "../../northstar/utils/MathUtil";
import { StyleConstants } from "../../northstar/utils/StyleContants";
import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection";
@@ -119,4 +119,4 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP
</svg>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 3d8f2d234..3394cb93d 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -325,15 +325,14 @@ export namespace DictationManager {
["open fields", {
action: (target: DocumentView) => {
- const kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
+ const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
}
}],
["new outline", {
action: (target: DocumentView) => {
- const newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
- newBox.autoHeight = true;
+ const newBox = Docs.Create.TextDocument("", { _width: 400, _height: 200, title: "My Outline", _autoHeight: true });
const proto = newBox.proto!;
const prompt = "Press alt + r to start dictating here...";
const head = 3;
@@ -379,7 +378,7 @@ export namespace DictationManager {
expression: /view as (freeform|stacking|masonry|schema|tree)/g,
action: (target: DocumentView, matches: RegExpExecArray) => {
const mode = CollectionViewType.valueOf(matches[1]);
- mode && (target.props.Document.viewType = mode);
+ mode && (target.props.Document._viewType = mode);
},
restrictTo: [DocumentType.COL]
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index df2f5fe3c..c05a2de96 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -94,6 +94,7 @@ export namespace DragManager {
readonly x: number,
readonly y: number,
readonly complete: DragCompleteEvent,
+ readonly shiftKey: boolean,
readonly altKey: boolean,
readonly metaKey: boolean,
readonly ctrlKey: boolean
@@ -205,7 +206,7 @@ export namespace DragManager {
// drag a button template and drop a new button
export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
const finishDrag = (e: DragCompleteEvent) => {
- const bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+ const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: title });
bd.onClick = ScriptField.MakeScript(script);
params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
initialize && initialize(bd);
@@ -340,7 +341,7 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
}
- if (e.shiftKey && CollectionDockingView.Instance) {
+ if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
AbortDrag();
finishDrag?.(new DragCompleteEvent(true, dragData));
CollectionDockingView.Instance.StartOtherDrag({
@@ -409,6 +410,7 @@ export namespace DragManager {
x: e.x,
y: e.y,
complete: complete,
+ shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 9e036d6c2..d0f1d86cb 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,44 +1,50 @@
import { DragManager } from "./DragManager";
-import { CollectionViewType } from "../views/collections/CollectionView";
import { Doc, DocListCast } from "../../new_fields/Doc";
import { DocumentType } from "../documents/DocumentTypes";
import { ObjectField } from "../../new_fields/ObjectField";
import { StrCast } from "../../new_fields/Types";
import { Docs } from "../documents/Documents";
-import { ScriptField } from "../../new_fields/ScriptField";
+import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
+import { RichTextField } from "../../new_fields/RichTextField";
+import { ImageField } from "../../new_fields/URLField";
-
-function makeTemplate(doc: Doc): boolean {
- const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+export function makeTemplate(doc: Doc): boolean {
+ const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
- docs.map(d => {
+ docs.forEach(d => {
if (!StrCast(d.title).startsWith("-")) {
- any = true;
- return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
+ } else if (d.type === DocumentType.COL || d.data instanceof RichTextField) {
+ any = makeTemplate(d) || any;
}
- if (d.type === DocumentType.COL) return makeTemplate(d);
- return false;
});
+ if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
+ if (!StrCast(layoutDoc.title).startsWith("-")) {
+ any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
+ }
+ }
return any;
}
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
- if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
- const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
- if (layoutDoc.type === DocumentType.COL) {
- layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
+ // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
+ if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
+ const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ if (layoutDoc.type === DocumentType.COL || layoutDoc.type === DocumentType.TEXT || layoutDoc.type === DocumentType.IMG) {
+ makeTemplate(layoutDoc);
} else {
- layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && !data.userDropAction;
+ (layoutDoc.layout instanceof Doc) && !data.userDropAction;
}
- dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+ layoutDoc.isTemplateDoc = true;
+ dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
- } else if (doc.viewType === CollectionViewType.Linear) {
+ } else if (doc.isButtonBar) {
dbox.ignoreClick = true;
}
data.droppedDocuments[i] = dbox;
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 5b5bffd8c..071015193 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -116,13 +116,13 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
formData.append(Utils.GenerateGuid(), file);
});
- collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
+ collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
runInAction(() => this.completed += batch.length);
});
await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
const path = Utils.prepend(clientAccessPath);
- const document = await Docs.Get.DocumentFromType(type, path, { width: 300, title: name });
+ const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
const { data, error } = exifData;
if (document) {
Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
@@ -145,8 +145,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
const offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0;
const options: DocumentOptions = {
title: `Import of ${directory}`,
- width: 1105,
- height: 500,
+ _width: 1105,
+ _height: 500,
x: NumCast(doc.x),
y: NumCast(doc.y) + offset
};
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 6a9486f83..ff909cc6b 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -14,9 +14,17 @@ export namespace ImageUtils {
return false;
}
const source = field.url.href;
- const response = await Networking.PostToServer("/inspectImage", { source });
- const { error, data } = response.exifData;
+ const {
+ contentSize,
+ nativeWidth,
+ nativeHeight,
+ exifData: { error, data }
+ } = await Networking.PostToServer("/inspectImage", { source });
document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ const proto = Doc.GetProto(document);
+ proto["data-nativeWidth"] = nativeWidth;
+ proto["data-nativeHeight"] = nativeHeight;
+ proto.contentSize = contentSize;
return data !== undefined;
};
diff --git a/src/client/util/InteractionUtils.ts b/src/client/util/InteractionUtils.tsx
index 8e1beecb7..8fc5e8098 100644
--- a/src/client/util/InteractionUtils.ts
+++ b/src/client/util/InteractionUtils.tsx
@@ -8,6 +8,20 @@ export namespace InteractionUtils {
const REACT_POINTER_PEN_BUTTON = 0;
const ERASER_BUTTON = 5;
+ export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+ const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+ return (
+ <polyline
+ points={pts}
+ style={{
+ fill: "none",
+ stroke: color,
+ strokeWidth: width
+ }}
+ />
+ );
+ }
+
export class MultiTouchEvent<T extends React.TouchEvent | TouchEvent> {
constructor(
readonly fingers: number,
@@ -60,7 +74,7 @@ export namespace InteractionUtils {
export function GetMyTargetTouches(mte: InteractionUtils.MultiTouchEvent<React.TouchEvent | TouchEvent>, prevPoints: Map<number, React.Touch>, ignorePen: boolean): React.Touch[] {
const myTouches = new Array<React.Touch>();
for (const pt of mte.touches) {
- if (ignorePen || (pt.radiusX > 1 && pt.radiusY > 1)) {
+ if (!ignorePen || ((pt as any).radiusX > 1 && (pt as any).radiusY > 1)) {
for (const tPt of mte.targetTouches) {
if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
if (pt && prevPoints.has(pt.identifier)) {
@@ -70,6 +84,9 @@ export namespace InteractionUtils {
}
}
}
+ // if (mte.touches.length !== myTouches.length) {
+ // throw Error("opo")
+ // }
return myTouches;
}
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index c028dbf8b..da3815181 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -5,9 +5,7 @@ import { Schema } from "prosemirror-model";
import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
import { splitListItem, wrapInList, } from "prosemirror-schema-list";
import { EditorState, Transaction, TextSelection } from "prosemirror-state";
-import { TooltipTextMenu } from "./TooltipTextMenu";
import { SelectionManager } from "./SelectionManager";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
diff --git a/src/client/util/RichTextMenu.scss b/src/client/util/RichTextMenu.scss
new file mode 100644
index 000000000..43cc23ecd
--- /dev/null
+++ b/src/client/util/RichTextMenu.scss
@@ -0,0 +1,121 @@
+@import "../views/globalCssVariables";
+
+.button-dropdown-wrapper {
+ position: relative;
+
+ .dropdown-button {
+ width: 15px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .dropdown-button-combined {
+ width: 50px;
+ display: flex;
+ justify-content: space-between;
+
+ svg:nth-child(2) {
+ margin-top: 2px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ background-color: #323232;
+ color: $light-color-secondary;
+ border: 1px solid #4d4d4d;
+ border-radius: 0 6px 6px 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ min-width: 150px;
+ padding: 5px;
+ font-size: 12px;
+ z-index: 10001;
+
+ button {
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+ padding: 6px;
+ margin: 5px 0;
+ font-size: 10px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ input {
+ color: black;
+ }
+}
+
+.link-menu {
+ .divider {
+ background-color: white;
+ height: 1px;
+ width: 100%;
+ }
+}
+
+.color-preview-button {
+ .color-preview {
+ width: 100%;
+ height: 3px;
+ margin-top: 3px;
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+ }
+}
+
+select {
+ background-color: #323232;
+ color: white;
+ border: 1px solid black;
+ // border-top: none;
+ // border-bottom: none;
+ font-size: 12px;
+ height: 100%;
+ margin-right: 3px;
+
+ &:focus,
+ &:hover {
+ background-color: black;
+ }
+
+ &::-ms-expand {
+ color: white;
+ }
+}
+
+.row-2 {
+ display: flex;
+ justify-content: space-between;
+
+ >div {
+ display: flex;
+ }
+} \ No newline at end of file
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
new file mode 100644
index 000000000..e07efe056
--- /dev/null
+++ b/src/client/util/RichTextMenu.tsx
@@ -0,0 +1,860 @@
+import React = require("react");
+import AntimodeMenu from "../views/AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./RichTextSchema";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
+import { MenuItem, Dropdown } from "prosemirror-menu";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { NumCast, Cast, StrCast } from "../../new_fields/Types";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../DocServer";
+import { Doc } from "../../new_fields/Doc";
+import { SelectionManager } from "./SelectionManager";
+import { LinkManager } from "./LinkManager";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+ static Instance: RichTextMenu;
+ public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
+
+ private view?: EditorView;
+ public editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+ public _brushMap: Map<string, Set<Mark>> = new Map();
+ private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
+ private fontColors: (string | undefined)[];
+ private highlightColors: (string | undefined)[];
+
+ @observable private boldActive: boolean = false;
+ @observable private italicsActive: boolean = false;
+ @observable private underlineActive: boolean = false;
+ @observable private strikethroughActive: boolean = false;
+ @observable private subscriptActive: boolean = false;
+ @observable private superscriptActive: boolean = false;
+
+ @observable private activeFontSize: string = "";
+ @observable private activeFontFamily: string = "";
+ @observable private activeListType: string = "";
+
+ @observable private brushIsEmpty: boolean = true;
+ @observable private brushMarks: Set<Mark> = new Set();
+ @observable private showBrushDropdown: boolean = false;
+
+ @observable private activeFontColor: string = "black";
+ @observable private showColorDropdown: boolean = false;
+
+ @observable private activeHighlightColor: string = "transparent";
+ @observable private showHighlightDropdown: boolean = false;
+
+ @observable private currentLink: string | undefined = "";
+ @observable private showLinkDropdown: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ RichTextMenu.Instance = this;
+ this._canFade = false;
+
+ this.fontSizeOptions = [
+ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
+ ];
+
+ this.fontFamilyOptions = [
+ { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+ ];
+
+ this.listTypeOptions = [
+ { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+ ];
+
+ this.fontColors = [
+ DarkPastelSchemaPalette.get("pink2"),
+ DarkPastelSchemaPalette.get("purple4"),
+ DarkPastelSchemaPalette.get("bluegreen1"),
+ DarkPastelSchemaPalette.get("yellow4"),
+ DarkPastelSchemaPalette.get("red2"),
+ DarkPastelSchemaPalette.get("bluegreen7"),
+ DarkPastelSchemaPalette.get("bluegreen5"),
+ DarkPastelSchemaPalette.get("orange1"),
+ "#757472",
+ "#000"
+ ];
+
+ this.highlightColors = [
+ PastelSchemaPalette.get("pink2"),
+ PastelSchemaPalette.get("purple4"),
+ PastelSchemaPalette.get("bluegreen1"),
+ PastelSchemaPalette.get("yellow4"),
+ PastelSchemaPalette.get("red2"),
+ PastelSchemaPalette.get("bluegreen7"),
+ PastelSchemaPalette.get("bluegreen5"),
+ PastelSchemaPalette.get("orange1"),
+ "white",
+ "transparent"
+ ];
+ }
+
+ @action
+ changeView(view: EditorView) {
+ this.view = view;
+ }
+
+ update(view: EditorView, lastState: EditorState | undefined) {
+ this.updateFromDash(view, lastState, this.editorProps);
+ }
+
+
+ public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
+ if (this.view) {
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
+ addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ return this.view.state.selection.$from.nodeAfter?.text || "";
+ }
+ return "";
+ }
+
+ @action
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
+ const state = view.state;
+ props && (this.editorProps = props);
+
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
+
+ // update active marks
+ const activeMarks = this.getActiveMarksOnSelection();
+ this.setActiveMarkButtons(activeMarks);
+
+ // update active font family and size
+ const active = this.getActiveFontStylesOnSelection();
+ const activeFamilies = active && active.get("families");
+ const activeSizes = active && active.get("sizes");
+
+ this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+
+ // update link in current selection
+ const targetTitle = await this.getTextLinkTargetTitle();
+ this.setCurrentLink(targetTitle);
+ }
+
+ setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
+ });
+ }
+ }
+ }
+
+ // finds font sizes and families in selection
+ getActiveFontStylesOnSelection() {
+ if (!this.view) return;
+
+ const activeFamilies: string[] = [];
+ const activeSizes: string[] = [];
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+
+ const styles = new Map<String, String[]>();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState<any>) {
+ const found = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ return found;
+ }
+
+ //finds all active marks on selection in given group
+ getActiveMarksOnSelection() {
+ if (!this.view) return;
+
+ const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+ //current selection
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ let activeMarks: MarkType[] = [];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode | null = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+ activeMarks = markGroup.filter(mark_type => {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ });
+ }
+ }
+ return activeMarks;
+ }
+
+ destroy() {
+ }
+
+ @action
+ setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+ if (!activeMarks) return;
+
+ this.boldActive = false;
+ this.italicsActive = false;
+ this.underlineActive = false;
+ this.strikethroughActive = false;
+ this.subscriptActive = false;
+ this.superscriptActive = false;
+
+ activeMarks.forEach(mark => {
+ switch (mark.name) {
+ case "strong": this.boldActive = true; break;
+ case "em": this.italicsActive = true; break;
+ case "underline": this.underlineActive = true; break;
+ case "strikethrough": this.strikethroughActive = true; break;
+ case "subscript": this.subscriptActive = true; break;
+ case "superscript": this.superscriptActive = true; break;
+ }
+ });
+ }
+
+ createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
+ const self = this;
+ function onClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ self.setActiveMarkButtons(self.getActiveMarksOnSelection());
+ }
+
+ return (
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} key={title} title={title} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ );
+ }
+
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ return <select onChange={onChange} key={key}>{items}</select>;
+ }
+
+ createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return <select onChange={e => onChange(e.target.value)} key={key}>{items}</select>;
+ }
+
+ changeFontSize = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch);
+ }
+
+ changeFontFamily = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch);
+ }
+
+ // TODO: remove doesn't work
+ //remove all node type and apply the passed-in one to the selected text
+ changeListType = (nodeType: NodeType | undefined) => {
+ if (!this.view) return;
+
+ if (nodeType === schema.nodes.bullet_list) {
+ wrapInList(nodeType)(this.view.state, this.view.dispatch);
+ } else {
+ const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view!.dispatch(tx2);
+ })) {
+ const tx2 = this.view.state.tr;
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view.dispatch(tx3);
+ }
+ }
+ }
+
+ insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ return true;
+ }
+
+ @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
+ // todo: add brushes to brushMap to save with a style name
+ onBrushNameKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
+ this._brushNameRef.current!.style.background = "lightGray";
+ }
+ }
+ _brushNameRef = React.createRef<HTMLInputElement>();
+
+ createBrushButton() {
+ const self = this;
+ function onBrushClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ }
+
+ let label = "Stored marks: ";
+ if (this.brushMarks && this.brushMarks.size > 0) {
+ this.brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ label += markType.name;
+ label += ", ";
+ });
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
+ }
+
+ const button =
+ <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks && this.brushMarks.size > 0 ? { backgroundColor: "121212" } : {}}>
+ <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transition: "transform 0.1s", transform: this.brushMarks && this.brushMarks.size > 0 ? "rotate(45deg)" : "" }} />
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>{label}</p>
+ <button onPointerDown={this.clearBrush}>Clear brush</button>
+ <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress}></input>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState<any>, dispatch: any) {
+ if (!this.view) return;
+
+ if (this.brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (selected_marks.size >= 0) {
+ this.brushMarks = selected_marks;
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ else {
+ const { from, to, $from } = this.view.state.selection;
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this.brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
+ }
+ }
+ else {
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ }
+
+ @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+ @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+ createColorButton() {
+ const self = this;
+ function onColorClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+ function changeColor(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveColor(color);
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+ <FontAwesomeIcon icon="palette" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown" >
+ <p>Change font color:</p>
+ <div className="color-wrapper">
+ {this.fontColors.map(color => {
+ if (color) {
+ return this.activeFontColor === color ?
+ <button className="color-button active" key={"active" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+ <button className="color-button" key={"other" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ return false;
+ }
+ this.setMark(colorMark, state, dispatch);
+ }
+
+ @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+ @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+ createHighlighterButton() {
+ const self = this;
+ function onHighlightClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+ function changeHighlight(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveHighlight(color);
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={onHighlightClick}>
+ <FontAwesomeIcon icon="highlighter" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>Change highlight color:</p>
+ <div className="color-wrapper">
+ {this.highlightColors.map(color => {
+ if (color) {
+ return this.activeHighlightColor === color ?
+ <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+ <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+
+ @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+ @action setCurrentLink(link: string) { this.currentLink = link; }
+
+ createLinkButton() {
+ const self = this;
+
+ function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = <FontAwesomeIcon icon="link" size="lg" />;
+
+ const dropdownContent =
+ <div className="dropdown link-menu">
+ <p>Linked to:</p>
+ <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+ <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+ <div className="divider"></div>
+ <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
+ );
+ }
+
+ async getTextLinkTargetTitle() {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
+ if (linkDoc instanceof Doc) {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
+ }
+ }
+ } else {
+ return href;
+ }
+ } else {
+ return link.attrs.title;
+ }
+ }
+ }
+
+ // TODO: should check for valid URL
+ makeLinkToURL = (target: String, lcoation: string) => {
+ if (!this.view) return;
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ deleteLink = () => {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
+ }
+ });
+ }
+ } else {
+ if (node) {
+ const { tr, schema, selection } = this.view.state;
+ const extension = this.linkExtend(selection.$anchor, href);
+ this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
+ }
+ }
+ }
+ }
+
+ linkExtend($start: ResolvedPos, href: string) {
+ const mark = this.view!.state.schema.marks.link;
+
+ let startIndex = $start.index();
+ let endIndex = $start.indexAfter();
+
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+ let startPos = $start.start();
+ let endPos = startPos;
+ for (let i = 0; i < endIndex; i++) {
+ const size = $start.parent.child(i).nodeSize;
+ if (i < startIndex) startPos += size;
+ endPos += size;
+ }
+ return { from: startPos, to: endPos };
+ }
+
+ reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ if (!this.view) return null;
+
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
+ ref_node = ref_node.child(0);
+ }
+ return ref_node;
+ }
+
+ @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+ @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
+
+ @action
+ toggleMenuPin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.fadeOut(true);
+ }
+ }
+
+ render() {
+
+ const row1 = <div className="antimodeMenu-row" key="row1">{[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ this.createColorButton(),
+ this.createHighlighterButton(),
+ this.createLinkButton(),
+ this.createBrushButton(),
+ this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+ ]}</div>;
+
+ const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
+ <div key="row">
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
+ </div>
+ <div key="button">
+ <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
+ </button>
+ {this.getDragger()}
+ </div>
+ </div>;
+
+ return (
+ <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.getElementWithRows([row1, row2], 2, false)}
+ </div>
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ view?: EditorView;
+ button: JSX.Element;
+ dropdownContent: JSX.Element;
+ openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+
+ @observable private showDropdown: boolean = false;
+ private ref: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.onBlur);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onBlur);
+ }
+
+ @action
+ setShowDropdown(show: boolean) {
+ this.showDropdown = show;
+ }
+ @action
+ toggleDropdown() {
+ this.showDropdown = !this.showDropdown;
+ }
+
+ onDropdownClick = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.view && this.props.view.focus();
+ this.toggleDropdown();
+ }
+
+ onBlur = (e: PointerEvent) => {
+ setTimeout(() => {
+ if (this.ref !== null && !this.ref.contains(e.target as Node)) {
+ this.setShowDropdown(false);
+ }
+ }, 0);
+ }
+
+ render() {
+ return (
+ <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
+ {this.props.openDropdownOnButton ?
+ <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+ {this.props.button}
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button> :
+ <>
+ {this.props.button}
+ <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button>
+ </>}
+
+ {this.showDropdown ? this.props.dropdownContent : (null)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 29b378299..8411cc6ee 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -2,14 +2,16 @@ import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from
import { schema } from "./RichTextSchema";
import { wrappingInputRule } from "./prosemirrorPatches";
import { NodeSelection, TextSelection } from "prosemirror-state";
-import { NumCast, Cast } from "../../new_fields/Types";
-import { Doc } from "../../new_fields/Doc";
+import { StrCast, Cast, NumCast } from "../../new_fields/Types";
+import { Doc, DataSym } from "../../new_fields/Doc";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { TooltipTextMenuManager } from "../util/TooltipTextMenu";
import { Docs, DocUtils } from "../documents/Documents";
import { Id } from "../../new_fields/FieldSymbols";
import { DocServer } from "../DocServer";
import { returnFalse, Utils } from "../../Utils";
+import RichTextMenu from "./RichTextMenu";
+import { RichTextField } from "../../new_fields/RichTextField";
+import { ComputedField } from "../../new_fields/ScriptField";
export const inpRules = {
rules: [
@@ -64,35 +66,69 @@ export const inpRules = {
// set the font size using #<font-size>
new InputRule(
- new RegExp(/^%([0-9]+)\s$/),
+ new RegExp(/%([0-9]+)\s$/),
(state, match, start, end) => {
const size = Number(match[1]);
- const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
- const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
- if (ruleProvider && heading) {
- (Cast(FormattedTextBox.FocusedBox!.props.Document, Doc) as Doc).heading = size;
- return state.tr.deleteRange(start, end);
- }
return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
}),
- // make current selection a hyperlink portal (assumes % was used to initiate an EnteringStyle mode)
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
new InputRule(
- new RegExp(/@$/),
+ new RegExp(/\[\[([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\]\]$/),
(state, match, start, end) => {
- if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null;
-
- const value = state.doc.textBetween(start, end);
- if (value) {
- DocServer.GetRefField(value).then(docx => {
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500, }, value);
- DocUtils.Publish(target, value, returnFalse, returnFalse);
+ if (!match[2]) {
+ const docId = match[1];
+ DocServer.GetRefField(docId).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true, }, docId);
+ DocUtils.Publish(target, docId, returnFalse, returnFalse);
DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
});
- const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + value), location: "onRight", title: value, targetId: value });
- return state.tr.addMark(start, end, link);
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docId), location: "onRight", title: docId, targetId: docId });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
}
- return state.tr;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: match[2]?.substring(1), docid: match[1] });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ new InputRule(
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const docId = match[1];
+ DocServer.GetRefField(docId).then(docx => {
+ if (!(docx instanceof Doc && docx)) {
+ const docx = Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true }, docId);
+ DocUtils.Publish(docx, docId, returnFalse, returnFalse);
+ }
+ });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid: docId, float: "right", fieldKey: match[2]?.substring(1), alias: Utils.GenerateGuid() });
+ const sm = state.storedMarks || undefined;
+ return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/##$/),
+ (state, match, start, end) => {
+ const schemaDoc = Doc.GetDataDoc((schema as any).Document);
+ const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!);
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
+ const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" });
+ textDocInline.title = inlineFieldKey; // give the annotation its own title
+ textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
+ textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
+ textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name });
+ 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 sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced;
}),
// stop using active style
new InputRule(
@@ -175,12 +211,6 @@ export const inpRules = {
(state, match, start, end) => {
const node = (state.doc.resolve(start) as any).nodeAfter;
const sm = state.storedMarks || undefined;
- const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
- const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "center";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }
const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
@@ -191,12 +221,6 @@ export const inpRules = {
(state, match, start, end) => {
const node = (state.doc.resolve(start) as any).nodeAfter;
const sm = state.storedMarks || undefined;
- const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
- const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "left";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }
const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
@@ -207,29 +231,11 @@ export const inpRules = {
(state, match, start, end) => {
const node = (state.doc.resolve(start) as any).nodeAfter;
const sm = state.storedMarks || undefined;
- const ruleProvider = FormattedTextBox.FocusedBox!.props.ruleProvider;
- const heading = NumCast(FormattedTextBox.FocusedBox!.props.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleAlign_" + heading] = "right";
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }
const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
state.tr;
return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
}),
new InputRule(
- new RegExp(/%#$/),
- (state, match, start, end) => {
- const target = Docs.Create.TextDocument({ width: 75, height: 35, backgroundColor: "yellow", annotationOn: FormattedTextBox.FocusedBox!.dataDoc, autoHeight: true, fontSize: 9, title: "inline comment" });
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docid: target[Id] });
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: target[Id], float: "right" });
- const sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced;//.setSelection(new NodeSelection(replaced.doc.resolve(end)));
- }),
- new InputRule(
new RegExp(/%\(/),
(state, match, start, end) => {
const node = (state.doc.resolve(start) as any).nodeAfter;
@@ -264,7 +270,7 @@ export const inpRules = {
new RegExp(/%[a-z]+$/),
(state, match, start, end) => {
const color = match[0].substring(1, match[0].length);
- const marks = TooltipTextMenuManager.Instance._brushMap.get(color);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
if (marks) {
const tr = state.tr.deleteRange(start, end);
return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index ef90a7294..287a1049b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,4 +1,4 @@
-import { reaction, IReactionDisposer } from "mobx";
+import { reaction, IReactionDisposer, observable, runInAction } from "mobx";
import { baseKeymap, toggleMark } from "prosemirror-commands";
import { redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
@@ -8,7 +8,7 @@ import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-s
import { StepMap } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, WidthSym, HeightSym } from "../../new_fields/Doc";
+import { Doc, WidthSym, HeightSym, DataSym, Field } from "../../new_fields/Doc";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { DocumentView } from "../views/nodes/DocumentView";
@@ -16,8 +16,12 @@ import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
import { Transform } from "./Transform";
import React = require("react");
-import { BoolCast, NumCast, Cast } from "../../new_fields/Types";
+import { BoolCast, NumCast, StrCast } from "../../new_fields/Types";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { ComputedField } from "../../new_fields/ScriptField";
+import { observer } from "mobx-react";
+import { Id } from "../../new_fields/FieldSymbols";
const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -165,7 +169,23 @@ export const nodes: { [index: string]: NodeSpec } = {
float: { default: "right" },
location: { default: "onRight" },
hidden: { default: false },
+ fieldKey: { default: "" },
docid: { default: "" },
+ alias: { default: "" }
+ },
+ group: "inline",
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashField: {
+ inline: true,
+ attrs: {
+ fieldKey: { default: "" },
+ docid: { default: "" }
},
group: "inline",
draggable: false,
@@ -306,7 +326,7 @@ export const marks: { [index: string]: MarkSpec } = {
}
}],
toDOM(node: any) {
- return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', { style: 'color: black' }];
+ return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0];
}
},
@@ -669,7 +689,7 @@ export class DashDocCommentView {
if (target) {
const expand = target.hidden;
const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
- view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs
+ view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs
setTimeout(() => {
expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { }
@@ -678,7 +698,7 @@ export class DashDocCommentView {
e.stopPropagation();
};
this._collapsed.onpointerenter = (e: any) => {
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
e.preventDefault();
e.stopPropagation();
};
@@ -703,7 +723,7 @@ export class DashDocView {
const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
}
- contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
this._textBox = tbox;
@@ -721,12 +741,6 @@ export class DashDocView {
this._dashSpan.style.height = node.attrs.height;
this._dashSpan.style.position = "absolute";
this._dashSpan.style.display = "inline-block";
- const removeDoc = () => {
- const pos = getPos();
- const ns = new NodeSelection(view.state.doc.resolve(pos));
- view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
- return true;
- };
this._dashSpan.onpointerleave = () => {
const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
if (ele) {
@@ -739,47 +753,26 @@ export class DashDocView {
(ele as HTMLDivElement).style.backgroundColor = "orange";
}
};
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
- if (dashDoc instanceof Doc) {
- self._dashDoc = dashDoc;
- dashDoc.hideSidebar = true;
- if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
- try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
- } catch (e) {
- console.log(e);
+ const removeDoc = () => {
+ const pos = getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ };
+ const alias = node.attrs.alias;
+
+ const docid = node.attrs.docid || tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+ DocServer.GetRefField(docid + alias).then(async dashDoc => {
+ if (!(dashDoc instanceof Doc)) {
+ alias && DocServer.GetRefField(docid).then(async dashDocBase => {
+ if (dashDocBase instanceof Doc) {
+ const aliasedDoc = Doc.MakeDelegate(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout_" + node.attrs.fieldKey;
+ self.doRender(aliasedDoc, removeDoc, node, view, getPos);
}
- }
- this._reactionDisposer && this._reactionDisposer();
- this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
- this._dashSpan.style.height = this._outer.style.height = dashDoc[HeightSym]() + "px";
- this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px";
});
- ReactDOM.render(<DocumentView
- Document={dashDoc}
- LibraryPath={tbox.props.LibraryPath}
- fitToBox={BoolCast(dashDoc.fitToBox)}
- addDocument={returnFalse}
- removeDocument={removeDoc}
- ruleProvider={undefined}
- ScreenToLocalTransform={this.getDocTransform}
- addDocTab={self._textBox.props.addDocTab}
- pinToPres={returnFalse}
- renderDepth={1}
- PanelWidth={self._dashDoc[WidthSym]}
- PanelHeight={self._dashDoc[HeightSym]}
- focus={self.outerFocus}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- dontRegisterView={false}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContentScaling={this.contentScaling}
- />, this._dashSpan);
+ } else {
+ self.doRender(dashDoc, removeDoc, node, view, getPos);
}
});
const self = this;
@@ -795,11 +788,110 @@ export class DashDocView {
this._outer.appendChild(this._dashSpan);
(this as any).dom = this._outer;
}
+ doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+ this._dashDoc = dashDoc;
+ dashDoc._hideSidebar = true;
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ const self = this;
+ const finalLayout = Doc.expandTemplateLayout(dashDoc, !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.Document) ? this._textBox.dataDoc : undefined);
+ if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ else {
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout._textTemplate = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer = reaction(() => [finalLayout[WidthSym](), finalLayout[HeightSym]()], (dim) => {
+ this._dashSpan.style.width = this._outer.style.width = dim[0] + "px";
+ this._dashSpan.style.height = this._outer.style.height = dim[1] + "px";
+ }, { fireImmediately: true });
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={!node.attrs.docid ? this._textBox.dataDoc : undefined}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={1}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ dontRegisterView={false}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ }
+ }
destroy() {
this._reactionDisposer && this._reactionDisposer();
}
}
+
+export class DashFieldView {
+ _fieldWrapper: HTMLDivElement;
+ _labelSpan: HTMLSpanElement;
+ _fieldSpan: HTMLSpanElement;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ @observable _dashDoc: Doc | undefined;
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._textBoxDoc = tbox.props.Document;
+ this._fieldWrapper = document.createElement("div");
+ this._fieldWrapper.style.width = node.attrs.width;
+ this._fieldWrapper.style.height = node.attrs.height;
+ this._fieldWrapper.style.position = "relative";
+ this._fieldWrapper.style.display = "inline";
+
+ this._fieldSpan = document.createElement("span");
+ this._fieldSpan.style.position = "relative";
+ this._fieldSpan.style.display = "inline";
+
+ this._labelSpan = document.createElement("span");
+ this._labelSpan.style.position = "relative";
+ this._labelSpan.style.display = "inline";
+ this._labelSpan.style.fontWeight = "bold";
+ this._labelSpan.style.fontSize = "larger";
+ this._labelSpan.innerHTML = `${node.attrs.fieldKey}: `;
+ if (node.attrs.docid) {
+ const self = this;
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => self._dashDoc = dashDoc));
+ } else {
+ this._dashDoc = tbox.props.DataDoc || tbox.dataDoc;
+ }
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => this._dashDoc?.[node.attrs.fieldKey], fval => this._fieldSpan.innerHTML = Field.toString(fval as Field), { fireImmediately: true });
+
+ this._fieldWrapper.appendChild(this._labelSpan);
+ this._fieldWrapper.appendChild(this._fieldSpan);
+ (this as any).dom = this._fieldWrapper;
+ }
+ destroy() {
+ this._reactionDisposer?.();
+ }
+}
+
export class OrderedListView {
update(node: any) {
return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
@@ -855,7 +947,8 @@ export class FootnoteView {
}),
new Plugin({
view(newView) {
- return FormattedTextBox.getToolTip(newView);
+ // TODO -- make this work with RichTextMenu
+ // return FormattedTextBox.getToolTip(newView);
}
})
],
@@ -963,7 +1056,7 @@ export class SummaryView {
view.dispatch(view.state.tr.
setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
- setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
+ setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
e.preventDefault();
e.stopPropagation();
this._collapsed.className = this.className(visible);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4612f10f4..86a7a620e 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,6 +3,8 @@ import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
import { List } from "../../new_fields/List";
+import { DocumentDecorations } from "../views/DocumentDecorations";
+import RichTextMenu from "./RichTextMenu";
export namespace SelectionManager {
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
new file mode 100644
index 000000000..7a0fb0741
--- /dev/null
+++ b/src/client/util/SettingsManager.scss
@@ -0,0 +1,136 @@
+@import "../views/globalCssVariables";
+
+.dialogue-box {
+ background-color: whitesmoke !important;
+ color: grey;
+ width: 450px;
+ height: 300px;
+
+ button {
+ background: $lighter-alt-accent;
+ outline: none;
+ border-radius: 5px;
+ border: 0px;
+ color: #fcfbf7;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ padding: 10px;
+ margin: 10px;
+ transition: transform 0.2s;
+ margin: 2px;
+ }
+}
+
+.settings-interface {
+ display: flex;
+ flex-direction: column;
+
+ button {
+ width: 100%;
+ align-self: center;
+ background: $darker-alt-accent;
+ margin-top: 4px;
+ }
+
+ .delete-button {
+ background: rgb(227, 86, 86);
+ }
+
+ .close-button {
+ position: absolute;
+ right: 1em;
+ top: 1em;
+ }
+
+ .settings-heading {
+ letter-spacing: .5em;
+ }
+
+
+ .settings-body {
+ display: flex;
+ justify-content: space-between;
+
+ .settings-type {
+ display: flex;
+ flex-direction: column;
+ flex-basis: 30%;
+
+ }
+
+ .settings-content {
+ padding-left: 1em;
+ padding-right: 1em;
+ display: flex;
+ flex-direction: column;
+ flex-basis: 70%;
+ justify-content: space-around;
+ text-align: left;
+
+ ::placeholder {
+ color: $intermediate-color;
+ }
+
+ input {
+ border-radius: 5px;
+ border: none;
+ padding: 4px;
+ min-width: 100%;
+ margin: 2px 0;
+ }
+
+ .error-text {
+ color: #C40233;
+ }
+
+ .success-text {
+ color: #009F6B;
+ }
+
+ p {
+ padding: 0 0 .1em .2em;
+ }
+
+ }
+ }
+
+ .focus-span {
+ text-decoration: underline;
+ }
+
+ h1 {
+ color: $dark-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 120%;
+ }
+
+ .container {
+ display: block;
+ position: relative;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 22px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ width: 700px;
+ min-width: 700px;
+ max-width: 700px;
+ text-align: left;
+ font-style: normal;
+ font-size: 15;
+ font-weight: normal;
+ padding: 0;
+
+ .padding {
+ padding: 0 0 0 20px;
+ color: black;
+ }
+
+
+
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
new file mode 100644
index 000000000..ff0b22381
--- /dev/null
+++ b/src/client/util/SettingsManager.tsx
@@ -0,0 +1,131 @@
+import { observable, runInAction, action } from "mobx";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { observer } from "mobx-react";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { SelectionManager } from "./SelectionManager";
+import "./SettingsManager.scss";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Networking } from "../Network";
+
+library.add(fa.faWindowClose);
+
+@observer
+export default class SettingsManager extends React.Component<{}> {
+ public static Instance: SettingsManager;
+ @observable private isOpen = false;
+ @observable private dialogueBoxOpacity = 1;
+ @observable private overlayOpacity = 0.4;
+ @observable private settingsContent = "password";
+ @observable private errorText = "";
+ @observable private successText = "";
+ private curr_password_ref = React.createRef<HTMLInputElement>();
+ private new_password_ref = React.createRef<HTMLInputElement>();
+ private new_confirm_ref = React.createRef<HTMLInputElement>();
+
+ public open = action(() => {
+ SelectionManager.DeselectAll();
+ this.isOpen = true;
+ });
+
+ public close = action(() => {
+ this.isOpen = false;
+ });
+
+ constructor(props: {}) {
+ super(props);
+ SettingsManager.Instance = this;
+ }
+
+ @action
+ private dispatchRequest = async () => {
+ const curr_pass = this.curr_password_ref.current?.value;
+ const new_pass = this.new_password_ref.current?.value;
+ const new_confirm = this.new_confirm_ref.current?.value;
+
+ if (!(curr_pass && new_pass && new_confirm)) {
+ this.changeAlertText("Hey, we're missing some fields!", "");
+ return;
+ }
+
+ const passwordBundle = {
+ curr_pass,
+ new_pass,
+ new_confirm
+ };
+
+ const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
+ if (error) {
+ this.changeAlertText("Uh oh! " + error[0].msg + "...", "");
+ return;
+ }
+
+ this.changeAlertText("", "Password successfully updated!");
+ }
+
+ @action
+ private changeAlertText = (errortxt: string, successtxt: string) => {
+ this.errorText = errortxt;
+ this.successText = successtxt;
+ }
+
+ @action
+ onClick = (event: any) => {
+ this.settingsContent = event.currentTarget.value;
+ this.errorText = "";
+ this.successText = "";
+ }
+
+ private get settingsInterface() {
+ return (
+ <div className={"settings-interface"}>
+ <div className="settings-heading">
+ <h1>settings</h1>
+ <div className={"close-button"} onClick={this.close}>
+ <FontAwesomeIcon icon={fa.faWindowClose} size={"lg"} />
+ </div>
+ </div>
+ <div className="settings-body">
+ <div className="settings-type">
+ <button onClick={this.onClick} value="password">reset password</button>
+ <button onClick={this.onClick} value="data">reset data</button>
+ </div>
+ {this.settingsContent === "password" ?
+ <div className="settings-content">
+ <input placeholder="current password" ref={this.curr_password_ref} />
+ <input placeholder="new password" ref={this.new_password_ref} />
+ <input placeholder="confirm new password" ref={this.new_confirm_ref} />
+ {this.errorText ? <div className="error-text">{this.errorText}</div> : undefined}
+ {this.successText ? <div className="success-text">{this.successText}</div> : undefined}
+ <button onClick={this.dispatchRequest}>submit</button>
+ <a href="/forgotPassword">forgot password?</a>
+
+ </div>
+ : undefined}
+ {this.settingsContent === "data" ?
+ <div className="settings-content">
+ <p>WARNING: <br />
+ THIS WILL ERASE ALL YOUR CURRENT DOCUMENTS STORED ON DASH. IF YOU WISH TO PROCEED, CLICK THE BUTTON BELOW.</p>
+ <button className="delete-button">DELETE</button>
+ </div>
+ : undefined}
+ </div>
+
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.settingsInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
+ overlayDisplayedOpacity={this.overlayOpacity}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
deleted file mode 100644
index b46675a04..000000000
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { EditorState } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import { FieldViewProps } from "../views/nodes/FieldView";
-import "./TooltipTextMenu.scss";
-
-//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
-export class TooltipLinkingMenu {
-
- private tooltip: HTMLElement;
- private view: EditorView;
- private editorProps: FieldViewProps;
-
- constructor(view: EditorView, editorProps: FieldViewProps) {
- this.view = view;
- this.editorProps = editorProps;
- this.tooltip = document.createElement("div");
- this.tooltip.className = "tooltipMenu linking";
-
- //add the div which is the tooltip
- view.dom.parentNode!.parentNode!.appendChild(this.tooltip);
-
- const target = "https://www.google.com";
-
- const link = document.createElement("a");
- link.href = target;
- link.textContent = target;
- link.target = "_blank";
- link.style.color = "white";
- this.tooltip.appendChild(link);
-
- this.update(view, undefined);
- }
-
- //updates the tooltip menu when the selection changes
- update(view: EditorView, lastState: EditorState | undefined) {
- const state = view.state;
- // Don't do anything if the document/selection didn't change
- if (lastState && lastState.doc.eq(state.doc) &&
- lastState.selection.eq(state.selection)) return;
-
- // Hide the tooltip if the selection is empty
- if (state.selection.empty) {
- this.tooltip.style.display = "none";
- return;
- }
-
- console.log("STORED:");
- console.log(state.doc.content.firstChild!.content);
-
- // Otherwise, reposition it and update its content
- this.tooltip.style.display = "";
- const { from, to } = state.selection;
- const start = view.coordsAtPos(from), end = view.coordsAtPos(to);
- // The box in which the tooltip is positioned, to use as base
- const box = this.tooltip.offsetParent!.getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- const left = Math.max((start.left + end.left) / 2, start.left + 3);
- this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px";
- const width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;
- const mid = Math.min(start.left, end.left) + width;
-
- this.tooltip.style.width = "auto";
- this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px";
- }
-
- destroy() { this.tooltip.remove(); }
-}
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss
deleted file mode 100644
index 2a38fe118..000000000
--- a/src/client/util/TooltipTextMenu.scss
+++ /dev/null
@@ -1,372 +0,0 @@
-@import "../views/globalCssVariables";
-.ProseMirror-menu-dropdown-wrap {
- display: inline-block;
- position: relative;
-}
-
-.ProseMirror-menu-dropdown {
- vertical-align: 1px;
- cursor: pointer;
- position: relative;
- padding: 0 15px 0 4px;
- background: white;
- border-radius: 2px;
- text-align: left;
- font-size: 12px;
- white-space: nowrap;
- margin-right: 4px;
-
- &:after {
- content: "";
- border-left: 4px solid transparent;
- border-right: 4px solid transparent;
- border-top: 4px solid currentColor;
- opacity: .6;
- position: absolute;
- right: 4px;
- top: calc(50% - 2px);
- }
-}
-
-.ProseMirror-menu-submenu-wrap {
- position: relative;
- margin-right: -4px;
-}
-
-.ProseMirror-menu-dropdown-menu,
-.ProseMirror-menu-submenu {
- font-size: 12px;
- background: white;
- border: 1px solid rgb(223, 223, 223);
- min-width: 40px;
- z-index: 50000;
- position: absolute;
- box-sizing: content-box;
-
- .ProseMirror-menu-dropdown-item {
- cursor: pointer;
- z-index: 100000;
- text-align: left;
- padding: 3px;
-
- &:hover {
- background-color: $light-color-secondary;
- }
- }
-}
-
-
-.ProseMirror-menu-submenu-label:after {
- content: "";
- border-top: 4px solid transparent;
- border-bottom: 4px solid transparent;
- border-left: 4px solid currentColor;
- opacity: .6;
- position: absolute;
- right: 4px;
- top: calc(50% - 4px);
-}
-
- .ProseMirror-icon {
- display: inline-block;
- // line-height: .8;
- // vertical-align: -2px; /* Compensate for padding */
- // padding: 2px 8px;
- cursor: pointer;
-
- &.ProseMirror-menu-disabled {
- cursor: default;
- }
-
- svg {
- fill:white;
- height: 1em;
- }
-
- span {
- vertical-align: text-top;
- }
- }
-
-.wrapper {
- position: absolute;
- pointer-events: all;
- display: flex;
- align-items: center;
- transform: translateY(-85px);
-
- height: 35px;
- background: #323232;
- border-radius: 6px;
- box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
-
-}
-
-.tooltipMenu, .basic-tools {
- z-index: 20000;
- pointer-events: all;
- padding: 3px;
- padding-bottom: 5px;
- display: flex;
- align-items: center;
-
- .ProseMirror-example-setup-style hr {
- padding: 2px 10px;
- border: none;
- margin: 1em 0;
- }
-
- .ProseMirror-example-setup-style hr:after {
- content: "";
- display: block;
- height: 1px;
- background-color: silver;
- line-height: 2px;
- }
-}
-
-.menuicon {
- width: 25px;
- height: 25px;
- cursor: pointer;
- text-align: center;
- line-height: 25px;
- margin: 0 2px;
- border-radius: 3px;
-
- &:hover {
- background-color: black;
-
- #link-drag {
- background-color: black;
- }
- }
-
- &> * {
- margin-top: 50%;
- margin-left: 50%;
- transform: translate(-50%, -50%);
- }
-
- svg {
- fill: inherit;
- width: 18px;
- height: 18px;
- }
-}
-
-.menuicon-active {
- width: 25px;
- height: 25px;
- cursor: pointer;
- text-align: center;
- line-height: 25px;
- margin: 0 2px;
- border-radius: 3px;
-
- &:hover {
- background-color: black;
- }
-
- &> * {
- margin-top: 50%;
- margin-left: 50%;
- transform: translate(-50%, -50%);
- }
-
- svg {
- fill: greenyellow;
- width: 18px;
- height: 18px;
- }
-}
-
-.colorPicker {
- position: relative;
-
- svg {
- width: 18px;
- height: 18px;
- // margin-top: 11px;
- }
-
- .buttonColor {
- position: absolute;
- top: 24px;
- left: 1px;
- width: 24px;
- height: 4px;
- margin-top: 0;
- }
-}
-
-#link-drag {
- background-color: #323232;
-}
-
-.underline svg {
- margin-top: 13px;
-}
-
- .font-size-indicator {
- font-size: 12px;
- padding-right: 0px;
- }
- .summarize{
- color: white;
- height: 20px;
- text-align: center;
- }
-
-
-.brush{
- display: inline-block;
- width: 1em;
- height: 1em;
- stroke-width: 0;
- stroke: currentColor;
- fill: currentColor;
- margin-right: 15px;
-}
-
-.brush-active{
- display: inline-block;
- width: 1em;
- height: 1em;
- stroke-width: 3;
- fill: greenyellow;
- margin-right: 15px;
-}
-
-.dragger-wrapper {
- color: #eee;
- height: 22px;
- padding: 0 5px;
- box-sizing: content-box;
- cursor: grab;
-
- .dragger {
- width: 18px;
- height: 100%;
- display: flex;
- justify-content: space-evenly;
- }
-
- .dragger-line {
- width: 2px;
- height: 100%;
- background-color: black;
- }
-}
-
-.button-dropdown-wrapper {
- display: flex;
- align-content: center;
-
- &:hover {
- background-color: black;
- }
-}
-
-.buttonSettings-dropdown {
-
- &.ProseMirror-menu-dropdown {
- width: 10px;
- height: 25px;
- margin: 0;
- padding: 0 2px;
- background-color: #323232;
- text-align: center;
-
- &:after {
- border-top: 4px solid white;
- right: 2px;
- }
-
- &:hover {
- background-color: black;
- }
- }
-
- &.ProseMirror-menu-dropdown-menu {
- min-width: 150px;
- left: -27px;
- top: 31px;
- background-color: #323232;
- border: 1px solid #4d4d4d;
- color: $light-color-secondary;
- // border: none;
- // border: 1px solid $light-color-secondary;
- border-radius: 0 6px 6px 6px;
- padding: 3px;
- box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
-
- .ProseMirror-menu-dropdown-item{
- cursor: default;
-
- &:last-child {
- border-bottom: none;
- }
-
- &:hover {
- background-color: #323232;
- }
-
- .button-setting, .button-setting-disabled {
- padding: 2px;
- border-radius: 2px;
- }
-
- .button-setting:hover {
- cursor: pointer;
- background-color: black;
- }
-
- .separated-button {
- border-top: 1px solid $light-color-secondary;
- padding-top: 6px;
- }
-
- input {
- color: black;
- border: none;
- border-radius: 1px;
- padding: 3px;
- }
-
- button {
- padding: 6px;
- background-color: #323232;
- border: 1px solid black;
- border-radius: 1px;
-
- &:hover {
- background-color: black;
- }
- }
- }
-
-
- }
-}
-
-.colorPicker-wrapper {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- margin-top: 3px;
- margin-left: -3px;
- width: calc(100% + 6px);
-}
-
-button.colorPicker {
- width: 20px;
- height: 20px;
- border-radius: 15px !important;
- margin: 3px;
- border: none !important;
-
- &.active {
- border: 2px solid white !important;
- }
-} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
deleted file mode 100644
index 8aa304fad..000000000
--- a/src/client/util/TooltipTextMenu.tsx
+++ /dev/null
@@ -1,1042 +0,0 @@
-import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
-import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
-import { wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import { Doc, Field, Opt } from "../../new_fields/Doc";
-import { Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { LinkManager } from "./LinkManager";
-import { schema } from "./RichTextSchema";
-import "./TooltipTextMenu.scss";
-import { Cast, NumCast, StrCast } from '../../new_fields/Types';
-import { updateBullets } from './ProsemirrorExampleTransfer';
-import { DocumentDecorations } from '../views/DocumentDecorations';
-import { SelectionManager } from './SelectionManager';
-import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
-const { toggleMark } = require("prosemirror-commands");
-
-//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
-export class TooltipTextMenu {
- public static Toolbar: HTMLDivElement | undefined;
-
- // editor state properties
- private view: EditorView;
- private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
-
- private fontStyles: Mark[] = [];
- private fontSizes: Mark[] = [];
- private _marksToDoms: Map<MarkType, HTMLSpanElement> = new Map();
- private _collapsed: boolean = false;
-
- // editor doms
- public tooltip: HTMLElement = document.createElement("div");
- private wrapper: HTMLDivElement = document.createElement("div");
-
- // editor button doms
- private colorDom?: Node;
- private colorDropdownDom?: Node;
- private linkDom?: Node;
- private highighterDom?: Node;
- private highlighterDropdownDom?: Node;
- private linkDropdownDom?: Node;
- private _brushdom?: Node;
- private _brushDropdownDom?: Node;
- private fontSizeDom?: Node;
- private fontStyleDom?: Node;
- private basicTools?: HTMLElement;
-
- static createDiv(className: string) { const div = document.createElement("div"); div.className = className; return div; }
- static createSpan(className: string) { const div = document.createElement("span"); div.className = className; return div; }
- constructor(view: EditorView) {
- this.view = view;
-
- // initialize the tooltip -- sets this.tooltip
- this.initTooltip(view);
-
- // initialize the wrapper
- this.wrapper = TooltipTextMenu.createDiv("wrapper");
- this.wrapper.appendChild(this.tooltip);
-
- TooltipTextMenu.Toolbar = this.wrapper;
- }
-
- private async initTooltip(view: EditorView) {
- const self = this;
- this.tooltip = TooltipTextMenu.createDiv("tooltipMenu");
- this.basicTools = TooltipTextMenu.createDiv("basic-tools");
-
- const svgIcon = (name: string, title: string = name, dpath: string) => {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("viewBox", "-100 -100 650 650");
- const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
- path.setAttributeNS(null, "d", dpath);
- svg.appendChild(path);
-
- const span = TooltipTextMenu.createSpan(name + " menuicon");
- span.title = title;
- span.appendChild(svg);
-
- return span;
- }
-
- const basicItems = [ // init basicItems in minimized toolbar -- paths to svgs are obtained from fontawesome
- { mark: schema.marks.strong, dom: svgIcon("strong", "Bold", "M333.49 238a122 122 0 0 0 27-65.21C367.87 96.49 308 32 233.42 32H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h31.87v288H34a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h209.32c70.8 0 134.14-51.75 141-122.4 4.74-48.45-16.39-92.06-50.83-119.6zM145.66 112h87.76a48 48 0 0 1 0 96h-87.76zm87.76 288h-87.76V288h87.76a56 56 0 0 1 0 112z") },
- { mark: schema.marks.em, dom: svgIcon("em", "Italic", "M320 48v32a16 16 0 0 1-16 16h-62.76l-80 320H208a16 16 0 0 1 16 16v32a16 16 0 0 1-16 16H16a16 16 0 0 1-16-16v-32a16 16 0 0 1 16-16h62.76l80-320H112a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h192a16 16 0 0 1 16 16z") },
- { mark: schema.marks.underline, dom: svgIcon("underline", "Underline", "M32 64h32v160c0 88.22 71.78 160 160 160s160-71.78 160-160V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H272a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h32v160a80 80 0 0 1-160 0V64h32a16 16 0 0 0 16-16V16a16 16 0 0 0-16-16H32a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16zm400 384H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z") },
- ];
- const items = [ // init items in full size toolbar
- { mark: schema.marks.strikethrough, dom: svgIcon("strikethrough", "Strikethrough", "M496 224H293.9l-87.17-26.83A43.55 43.55 0 0 1 219.55 112h66.79A49.89 49.89 0 0 1 331 139.58a16 16 0 0 0 21.46 7.15l42.94-21.47a16 16 0 0 0 7.16-21.46l-.53-1A128 128 0 0 0 287.51 32h-68a123.68 123.68 0 0 0-123 135.64c2 20.89 10.1 39.83 21.78 56.36H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h480a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm-180.24 96A43 43 0 0 1 336 356.45 43.59 43.59 0 0 1 292.45 400h-66.79A49.89 49.89 0 0 1 181 372.42a16 16 0 0 0-21.46-7.15l-42.94 21.47a16 16 0 0 0-7.16 21.46l.53 1A128 128 0 0 0 224.49 480h68a123.68 123.68 0 0 0 123-135.64 114.25 114.25 0 0 0-5.34-24.36z") },
- { mark: schema.marks.superscript, dom: svgIcon("superscript", "Superscript", "M496 160h-16V16a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 64h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
- { mark: schema.marks.subscript, dom: svgIcon("subscript", "Subscript", "M496 448h-16V304a16 16 0 0 0-16-16h-48a16 16 0 0 0-14.29 8.83l-16 32A16 16 0 0 0 400 352h16v96h-16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h96a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zM336 64h-67a16 16 0 0 0-13.14 6.87l-79.9 115-79.9-115A16 16 0 0 0 83 64H16A16 16 0 0 0 0 80v48a16 16 0 0 0 16 16h33.48l77.81 112-77.81 112H16a16 16 0 0 0-16 16v48a16 16 0 0 0 16 16h67a16 16 0 0 0 13.14-6.87l79.9-115 79.9 115A16 16 0 0 0 269 448h67a16 16 0 0 0 16-16v-48a16 16 0 0 0-16-16h-33.48l-77.81-112 77.81-112H336a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16z") },
- ];
-
- basicItems.map(({ dom, mark }) => this.basicTools?.appendChild(dom.cloneNode(true)));
- basicItems.concat(items).forEach(({ dom, mark }) => {
- this.tooltip.appendChild(dom);
- this._marksToDoms.set(mark, dom);
-
- //pointer down handler to activate button effects
- dom.addEventListener("pointerdown", e => {
- this.view.focus();
- if (dom.contains(e.target as Node)) {
- e.preventDefault();
- e.stopPropagation();
- toggleMark(mark)(this.view.state, this.view.dispatch, this.view);
- this.updateHighlightStateOfButtons();
- }
- });
- });
-
- // summarize menu
- this.highighterDom = this.createHighlightTool().render(this.view).dom;
- this.highlighterDropdownDom = this.createHighlightDropdown().render(this.view).dom;
- this.tooltip.appendChild(this.highighterDom);
- this.tooltip.appendChild(this.highlighterDropdownDom);
-
- // color menu
- this.colorDom = this.createColorTool().render(this.view).dom;
- this.colorDropdownDom = this.createColorDropdown().render(this.view).dom;
- this.tooltip.appendChild(this.colorDom);
- this.tooltip.appendChild(this.colorDropdownDom);
-
- // link menu
- this.linkDom = this.createLinkTool().render(this.view).dom;
- this.linkDropdownDom = this.createLinkDropdown("").render(this.view).dom;
- this.tooltip.appendChild(this.linkDom);
- this.tooltip.appendChild(this.linkDropdownDom);
-
- // list of font styles
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 7 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 8 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 9 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 10 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 12 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 14 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 16 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 18 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 20 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 24 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 32 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 48 }));
- this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 72 }));
-
- // font sizes
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" }));
- this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" }));
-
-
- // init brush tool
- this._brushdom = this.createBrushTool().render(this.view).dom;
- this.tooltip.appendChild(this._brushdom);
- this._brushDropdownDom = this.createBrushDropdown().render(this.view).dom;
- this.tooltip.appendChild(this._brushDropdownDom);
-
- // summarizer tool
- const summarizer = new MenuItem({
- title: "Summarize",
- label: "Summarize",
- icon: icons.join,
- css: "fill:white;",
- class: "menuicon",
- execEvent: "",
- run: (state, dispatch) => TooltipTextMenu.insertSummarizer(state, dispatch)
- });
- this.tooltip.appendChild(summarizer.render(this.view).dom);
-
- // list types dropdown
- const listDropdownTypes = [{ mapStyle: "bullet", label: ":" }, { mapStyle: "decimal", label: "1.1" }, { mapStyle: "multi", label: "A.1" }, { label: "X" }];
- const listTypes = new Dropdown(listDropdownTypes.map(({ mapStyle, label }) =>
- new MenuItem({
- title: "Set Bullet Style",
- label: label,
- execEvent: "",
- class: "dropdown-item",
- css: "color: black; width: 40px;",
- enable() { return true; },
- run() {
- const marks = self.view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
- if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
- const tx3 = updateBullets(tx2, schema, mapStyle);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
-
- view.dispatch(tx2);
- })) {
- const tx2 = view.state.tr;
- const tx3 = updateBullets(tx2, schema, mapStyle);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
-
- view.dispatch(tx3);
- }
- }
- })), { label: ":", css: "color:black; width: 40px;" });
- this.tooltip.appendChild(listTypes.render(this.view).dom);
-
- await this.updateFromDash(view, undefined, undefined);
-
- const draggerWrapper = TooltipTextMenu.createDiv("dragger-wrapper");
- const dragger = TooltipTextMenu.createDiv("dragger");
- dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
- dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
- dragger.appendChild(TooltipTextMenu.createSpan("dragger-line"));
- draggerWrapper.appendChild(dragger);
- this.wrapper.appendChild(draggerWrapper);
- this.setupDragElementInteractions(draggerWrapper);
- }
-
- setupDragElementInteractions(elmnt: HTMLElement) {
- var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
- if (elmnt) {
- // if present, the header is where you move the DIV from:
- elmnt.onpointerdown = dragPointerDown;
- elmnt.ondblclick = onClick;
- }
- const self = this;
-
- function dragPointerDown(e: PointerEvent) {
- e = e || window.event;
- e.preventDefault();
- // get the mouse cursor position at startup:
- pos3 = e.clientX;
- pos4 = e.clientY;
- document.onpointerup = closeDragElement;
- // call a function whenever the cursor moves:
- document.onpointermove = elementDrag;
- }
-
- function onClick(e: MouseEvent) {
- self._collapsed = !self._collapsed;
- const children = self.wrapper.childNodes;
- if (self._collapsed && children.length > 0) {
- self.wrapper.removeChild(self.tooltip);
- self.basicTools && self.wrapper.prepend(self.basicTools);
- }
- else {
- self.wrapper.prepend(self.tooltip);
- self.basicTools && self.wrapper.removeChild(self.basicTools);
- }
- }
-
- function elementDrag(e: PointerEvent) {
- e = e || window.event;
- //e.preventDefault();
- // calculate the new cursor position:
- pos1 = pos3 - e.clientX;
- pos2 = pos4 - e.clientY;
- pos3 = e.clientX;
- pos4 = e.clientY;
- // set the element's new position:
- // elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
- // elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
-
- self.wrapper.style.top = (self.wrapper.offsetTop - pos2) + "px";
- self.wrapper.style.left = (self.wrapper.offsetLeft - pos1) + "px";
- }
-
- function closeDragElement() {
- // stop moving when mouse button is released:
- document.onpointerup = null;
- document.onpointermove = null;
- }
- }
-
- //label of dropdown will change to given label
- updateFontSizeDropdown(label: string) {
- //font SIZES
- const fontSizeBtns: MenuItem[] = [];
- const self = this;
- this.fontSizes.forEach(mark =>
- fontSizeBtns.push(new MenuItem({
- title: "Set Font Size",
- label: String(mark.attrs.fontSize),
- execEvent: "",
- class: "dropdown-item",
- css: "color: black; width: 50px;",
- enable() { return true; },
- run() {
- const size = mark.attrs.fontSize;
- if (size) { self.updateFontSizeDropdown(String(size) + " pt"); }
- if (self.editorProps) {
- const ruleProvider = self.editorProps.ruleProvider;
- const heading = NumCast(self.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleSize_" + heading] = size;
- }
- }
- TooltipTextMenu.setMark(self.view.state.schema.marks.pFontSize.create({ fontSize: size }), self.view.state, self.view.dispatch);
- }
- })));
-
- const newfontSizeDom = (new Dropdown(fontSizeBtns, { label: label, css: "color:black; min-width: 60px;" }) as MenuItem).render(this.view).dom;
- if (this.fontSizeDom) {
- this.tooltip.replaceChild(newfontSizeDom, this.fontSizeDom);
- }
- else {
- this.tooltip.appendChild(newfontSizeDom);
- }
- this.fontSizeDom = newfontSizeDom;
- }
-
- //label of dropdown will change to given label
- updateFontStyleDropdown(label: string) {
- //font STYLES
- const fontBtns: MenuItem[] = [];
- const self = this;
- this.fontStyles.forEach(mark =>
- fontBtns.push(new MenuItem({
- title: "Set Font Family",
- label: mark.attrs.family,
- execEvent: "",
- class: "dropdown-item",
- css: "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;",
- enable() { return true; },
- run() {
- const fontName = mark.attrs.family;
- if (fontName) { self.updateFontStyleDropdown(fontName); }
- if (self.editorProps) {
- const ruleProvider = self.editorProps.ruleProvider;
- const heading = NumCast(self.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleFont_" + heading] = fontName;
- }
- }
- TooltipTextMenu.setMark(self.view.state.schema.marks.pFontFamily.create({ family: fontName }), self.view.state, self.view.dispatch);
- }
- })));
-
- const newfontStyleDom = (new Dropdown(fontBtns, { label: label, css: "color:black; width: 125px;" }) as MenuItem).render(this.view).dom;
- if (this.fontStyleDom) {
- this.tooltip.replaceChild(newfontStyleDom, this.fontStyleDom);
- }
- else {
- this.tooltip.appendChild(newfontStyleDom);
- }
- this.fontStyleDom = newfontStyleDom;
- }
- async getTextLinkTargetTitle() {
- const node = this.view.state.selection.$from.nodeAfter;
- const link = node && node.marks.find(m => m.type.name === "link");
- if (link) {
- const href = link.attrs.href;
- if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (linkclicked) {
- const linkDoc = await DocServer.GetRefField(linkclicked);
- if (linkDoc instanceof Doc) {
- const anchor1 = await Cast(linkDoc.anchor1, Doc);
- const anchor2 = await Cast(linkDoc.anchor2, Doc);
- const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
- if (currentDoc && anchor1 && anchor2) {
- if (Doc.AreProtosEqual(currentDoc, anchor1)) {
- return StrCast(anchor2.title);
- }
- if (Doc.AreProtosEqual(currentDoc, anchor2)) {
- return StrCast(anchor1.title);
- }
- }
- }
- }
- } else {
- return href;
- }
- } else {
- return link.attrs.title;
- }
- }
- }
-
- // LINK TOOL
- createLinkTool(active: boolean = false) {
- return new MenuItem({
- title: "Link tool",
- label: "Link tool",
- icon: icons.link,
- css: "fill:white;",
- class: active ? "menuicon-active" : "menuicon",
- execEvent: "",
- run: async (state, dispatch) => { },
- active: (state) => true
- });
- }
-
- createLinkDropdown(targetTitle: string) {
- const input = document.createElement("input");
-
- // menu item for input for hyperlink url
- // TODO: integrate search to allow users to search for a doc to link to
- const linkInfo = new MenuItem({
- title: "",
- execEvent: "",
- class: "button-setting-disabled",
- css: "",
- render() {
- const p = document.createElement("p");
- p.textContent = "Linked to:";
-
- input.type = "text";
- input.placeholder = "Enter URL";
- if (targetTitle) input.value = targetTitle;
- input.onclick = (e: MouseEvent) => {
- input.select();
- input.focus();
- };
-
- const div = document.createElement("div");
- div.appendChild(p);
- div.appendChild(input);
- return div;
- },
- enable() { return false; },
- run(p1, p2, p3, event) { event.stopPropagation(); }
- });
-
- // menu item to update/apply the hyperlink to the selected text
- const linkApply = new MenuItem({
- title: "",
- execEvent: "",
- class: "",
- css: "",
- render() {
- const button = document.createElement("button");
- button.className = "link-url-button";
- button.textContent = "Apply hyperlink";
- return button;
- },
- enable() { return false; },
- run: async (state, dispatch, view, event) => {
- event.stopPropagation();
- let node = this.view.state.selection.$from.nodeAfter;
- let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: input.value, location: "onRight" });
- this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
- this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
- node = this.view.state.selection.$from.nodeAfter;
- link = node && node.marks.find(m => m.type.name === "link");
-
- // update link menu
- const linkDom = self.createLinkTool(true).render(self.view).dom;
- const linkDropdownDom = self.createLinkDropdown(await self.getTextLinkTargetTitle()).render(self.view).dom;
- self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom);
- self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom);
- self.linkDom = linkDom;
- self.linkDropdownDom = linkDropdownDom;
- }
- });
-
- // menu item to remove the link
- // TODO: allow this to be undoable
- const self = this;
- const deleteLink = new MenuItem({
- title: "Delete link",
- execEvent: "",
- class: "separated-button",
- css: "",
- render() {
- const button = document.createElement("button");
- button.textContent = "Remove link";
-
- const wrapper = document.createElement("div");
- wrapper.appendChild(button);
- return wrapper;
- },
- enable() { return true; },
- async run() {
- // delete the link
- const node = self.view.state.selection.$from.nodeAfter;
- const link = node && node.marks.find(m => m.type === self.view.state.schema.marks.link);
- const href = link!.attrs.href;
- if (href?.indexOf(Utils.prepend("/doc/")) === 0) {
- const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- linkclicked && DocServer.GetRefField(linkclicked).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- LinkManager.Instance.deleteLink(linkDoc);
- self.view.dispatch(self.view.state.tr.removeMark(self.view.state.selection.from, self.view.state.selection.to, self.view.state.schema.marks.link));
- }
- });
- }
- // update link menu
- const linkDom = self.createLinkTool(false).render(self.view).dom;
- const linkDropdownDom = self.createLinkDropdown("").render(self.view).dom;
- self.linkDom && self.tooltip.replaceChild(linkDom, self.linkDom);
- self.linkDropdownDom && self.tooltip.replaceChild(linkDropdownDom, self.linkDropdownDom);
- self.linkDom = linkDom;
- self.linkDropdownDom = linkDropdownDom;
- }
- });
-
- return new Dropdown(targetTitle ? [linkInfo, linkApply, deleteLink] : [linkInfo, linkApply], { class: "buttonSettings-dropdown" }) as MenuItem;
- }
-
- public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
- const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
- this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
- addMark(this.view.state.selection.from, this.view.state.selection.to, link));
- return this.view.state.selection.$from.nodeAfter?.text || "";
- }
-
- // SUMMARIZER TOOL
- static insertSummarizer(state: EditorState<any>, dispatch: any) {
- if (!state.selection.empty) {
- const mark = state.schema.marks.summarize.create();
- const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
- const content = tr.selection.content();
- const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
- dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
- }
- }
-
- // HIGHLIGHTER TOOL
- createHighlightTool() {
- return new MenuItem({
- title: "Highlight",
- css: "fill:white;",
- class: "menuicon",
- execEvent: "",
- render() {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("viewBox", "-100 -100 650 650");
- const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
- path.setAttributeNS(null, "d", "M0 479.98L99.92 512l35.45-35.45-67.04-67.04L0 479.98zm124.61-240.01a36.592 36.592 0 0 0-10.79 38.1l13.05 42.83-50.93 50.94 96.23 96.23 50.86-50.86 42.74 13.08c13.73 4.2 28.65-.01 38.15-10.78l35.55-41.64-173.34-173.34-41.52 35.44zm403.31-160.7l-63.2-63.2c-20.49-20.49-53.38-21.52-75.12-2.35L190.55 183.68l169.77 169.78L530.27 154.4c19.18-21.74 18.15-54.63-2.35-75.13z");
- svg.appendChild(path);
-
- const color = TooltipTextMenu.createDiv("buttonColor");
- color.style.backgroundColor = TooltipTextMenuManager.Instance.highlighter.toString();
-
- const wrapper = TooltipTextMenu.createDiv("colorPicker");
- wrapper.appendChild(svg);
- wrapper.appendChild(color);
- return wrapper;
- },
- run: (state, dispatch) => TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, state, dispatch)
- });
- }
-
- static insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
- if (!state.selection.empty) {
- toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
- }
- }
-
- createHighlightDropdown() {
- // menu item for color picker
- const self = this;
- const colors = new MenuItem({
- title: "",
- execEvent: "",
- class: "button-setting-disabled",
- css: "",
- render() {
- const p = document.createElement("p");
- p.textContent = "Change highlight:";
-
- const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper");
-
- const colors = [
- PastelSchemaPalette.get("pink2"),
- PastelSchemaPalette.get("purple4"),
- PastelSchemaPalette.get("bluegreen1"),
- PastelSchemaPalette.get("yellow4"),
- PastelSchemaPalette.get("red2"),
- PastelSchemaPalette.get("bluegreen7"),
- PastelSchemaPalette.get("bluegreen5"),
- PastelSchemaPalette.get("orange1"),
- "white",
- "transparent"
- ];
-
- colors.forEach(color => {
- const button = document.createElement("button");
- button.className = color === TooltipTextMenuManager.Instance.highlighter ? "colorPicker active" : "colorPicker";
- if (color) {
- button.style.backgroundColor = color;
- button.textContent = color === "transparent" ? "X" : "";
- button.onclick = e => {
- TooltipTextMenuManager.Instance.highlighter = color;
-
- TooltipTextMenu.insertHighlight(TooltipTextMenuManager.Instance.highlighter, self.view.state, self.view.dispatch);
-
- // update color menu
- const highlightDom = self.createHighlightTool().render(self.view).dom;
- const highlightDropdownDom = self.createHighlightDropdown().render(self.view).dom;
- self.highighterDom && self.tooltip.replaceChild(highlightDom, self.highighterDom);
- self.highlighterDropdownDom && self.tooltip.replaceChild(highlightDropdownDom, self.highlighterDropdownDom);
- self.highighterDom = highlightDom;
- self.highlighterDropdownDom = highlightDropdownDom;
- };
- }
- colorsWrapper.appendChild(button);
- });
-
- const div = document.createElement("div");
- div.appendChild(p);
- div.appendChild(colorsWrapper);
- return div;
- },
- enable() { return false; },
- run(p1, p2, p3, event) {
- event.stopPropagation();
- }
- });
-
- return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
- }
-
- // COLOR TOOL
- createColorTool() {
- return new MenuItem({
- title: "Color",
- css: "fill:white;",
- class: "menuicon",
- execEvent: "",
- render() {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("viewBox", "-100 -100 650 650");
- const path = document.createElementNS('http://www.w3.org/2000/svg', "path");
- path.setAttributeNS(null, "d", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z");
- svg.appendChild(path);
-
- const color = TooltipTextMenu.createDiv("buttonColor");
- color.style.backgroundColor = TooltipTextMenuManager.Instance.color.toString();
-
- const wrapper = TooltipTextMenu.createDiv("colorPicker");
- wrapper.appendChild(svg);
- wrapper.appendChild(color);
- return wrapper;
- },
- run: (state, dispatch) => TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, state, dispatch)
- });
- }
-
- static insertColor(color: String, state: EditorState<any>, dispatch: any) {
- const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
- if (state.selection.empty) {
- dispatch(state.tr.addStoredMark(colorMark));
- } else {
- this.setMark(colorMark, state, dispatch);
- }
- }
-
- createColorDropdown() {
- // menu item for color picker
- const self = this;
- const colors = new MenuItem({
- title: "",
- execEvent: "",
- class: "button-setting-disabled",
- css: "",
- render() {
- const p = document.createElement("p");
- p.textContent = "Change color:";
-
- const colorsWrapper = TooltipTextMenu.createDiv("colorPicker-wrapper");
-
- const colors = [
- DarkPastelSchemaPalette.get("pink2"),
- DarkPastelSchemaPalette.get("purple4"),
- DarkPastelSchemaPalette.get("bluegreen1"),
- DarkPastelSchemaPalette.get("yellow4"),
- DarkPastelSchemaPalette.get("red2"),
- DarkPastelSchemaPalette.get("bluegreen7"),
- DarkPastelSchemaPalette.get("bluegreen5"),
- DarkPastelSchemaPalette.get("orange1"),
- "#757472",
- "#000"
- ];
-
- colors.forEach(color => {
- const button = document.createElement("button");
- button.className = color === TooltipTextMenuManager.Instance.color ? "colorPicker active" : "colorPicker";
- if (color) {
- button.style.backgroundColor = color;
- button.onclick = e => {
- TooltipTextMenuManager.Instance.color = color;
-
- TooltipTextMenu.insertColor(TooltipTextMenuManager.Instance.color, self.view.state, self.view.dispatch);
-
- // update color menu
- const colorDom = self.createColorTool().render(self.view).dom;
- const colorDropdownDom = self.createColorDropdown().render(self.view).dom;
- self.colorDom && self.tooltip.replaceChild(colorDom, self.colorDom);
- self.colorDropdownDom && self.tooltip.replaceChild(colorDropdownDom, self.colorDropdownDom);
- self.colorDom = colorDom;
- self.colorDropdownDom = colorDropdownDom;
- };
- }
- colorsWrapper.appendChild(button);
- });
-
- const div = document.createElement("div");
- div.appendChild(p);
- div.appendChild(colorsWrapper);
- return div;
- },
- enable() { return false; },
- run(p1, p2, p3, event) { event.stopPropagation(); }
- });
-
- return new Dropdown([colors], { class: "buttonSettings-dropdown" }) as MenuItem;
- }
-
- // BRUSH TOOL
- createBrushTool(active: boolean = false) {
- const icon = {
- height: 32, width: 32,
- path: "M30.828 1.172c-1.562-1.562-4.095-1.562-5.657 0l-5.379 5.379-3.793-3.793-4.243 4.243 3.326 3.326-14.754 14.754c-0.252 0.252-0.358 0.592-0.322 0.921h-0.008v5c0 0.552 0.448 1 1 1h5c0 0 0.083 0 0.125 0 0.288 0 0.576-0.11 0.795-0.329l14.754-14.754 3.326 3.326 4.243-4.243-3.793-3.793 5.379-5.379c1.562-1.562 1.562-4.095 0-5.657zM5.409 30h-3.409v-3.409l14.674-14.674 3.409 3.409-14.674 14.674z"
- };
- const self = this;
- return new MenuItem({
- title: "Brush tool",
- label: "Brush tool",
- icon: icon,
- css: "fill:white;",
- class: active ? "menuicon-active" : "menuicon",
- execEvent: "",
- run: (state, dispatch) => {
- this.brush_function(state, dispatch);
-
- // update dropdown with marks
- const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
- self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
- self._brushDropdownDom = newBrushDropdowndom;
- },
- active: (state) => true
- });
- }
-
- brush_function(state: EditorState<any>, dispatch: any) {
- if (TooltipTextMenuManager.Instance._brushIsEmpty) {
- // get marks in the selection
- const selected_marks = new Set<Mark>();
- const { from, to } = state.selection as TextSelection;
- state.doc.nodesBetween(from, to, (node) => node.marks?.forEach(m => selected_marks.add(m)));
-
- if (this._brushdom && selected_marks.size >= 0) {
- TooltipTextMenuManager.Instance._brushMarks = selected_marks;
- const newbrush = this.createBrushTool(true).render(this.view).dom;
- this.tooltip.replaceChild(newbrush, this._brushdom);
- this._brushdom = newbrush;
- TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
- }
- }
- else {
- const { from, to, $from } = this.view.state.selection;
- if (this._brushdom) {
- if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
- if (TooltipTextMenuManager.Instance._brushMarks && to - from > 0) {
- this.view.dispatch(this.view.state.tr.removeMark(from, to));
- Array.from(TooltipTextMenuManager.Instance._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
- TooltipTextMenu.setMark(mark, this.view.state, this.view.dispatch);
- });
- }
- }
- else {
- const newbrush = this.createBrushTool(false).render(this.view).dom;
- this.tooltip.replaceChild(newbrush, this._brushdom);
- this._brushdom = newbrush;
- TooltipTextMenuManager.Instance._brushIsEmpty = !TooltipTextMenuManager.Instance._brushIsEmpty;
- }
- }
- }
- }
-
- createBrushDropdown(active: boolean = false) {
- let label = "Stored marks: ";
- if (TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0) {
- TooltipTextMenuManager.Instance._brushMarks.forEach((mark: Mark) => label += mark.type.name + ", ");
- label = label.substring(0, label.length - 2);
- } else {
- label = "No marks are currently stored";
- }
-
- const brushInfo = new MenuItem({
- title: "",
- label: label,
- execEvent: "",
- class: "button-setting-disabled",
- css: "",
- enable() { return false; },
- run(p1, p2, p3, event) { event.stopPropagation(); }
- });
-
- const self = this;
- const input = document.createElement("input");
- const clearBrush = new MenuItem({
- title: "Clear brush",
- execEvent: "",
- class: "separated-button",
- css: "",
- render() {
- const button = document.createElement("button");
- button.textContent = "Clear brush";
-
- input.textContent = "editme";
- input.style.width = "75px";
- input.style.height = "30px";
- input.style.background = "white";
- input.setAttribute("contenteditable", "true");
- input.style.whiteSpace = "nowrap";
- input.type = "text";
- input.placeholder = "Enter URL";
- input.onpointerdown = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
- };
- input.onclick = (e: MouseEvent) => {
- input.select();
- input.focus();
- };
- input.onkeypress = (e: KeyboardEvent) => {
- if (e.key === "Enter") {
- TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMap.set(input.value, TooltipTextMenuManager.Instance._brushMarks);
- input.style.background = "lightGray";
- }
- };
-
- const wrapper = document.createElement("div");
- wrapper.appendChild(input);
- wrapper.appendChild(button);
- return wrapper;
- },
- enable() { return true; },
- run() {
- TooltipTextMenuManager.Instance._brushIsEmpty = true;
- TooltipTextMenuManager.Instance._brushMarks = new Set();
-
- // update brush tool
- // TODO: this probably isn't very clean
- const newBrushdom = self.createBrushTool().render(self.view).dom;
- self._brushdom && self.tooltip.replaceChild(newBrushdom, self._brushdom);
- self._brushdom = newBrushdom;
- const newBrushDropdowndom = self.createBrushDropdown().render(self.view).dom;
- self._brushDropdownDom && self.tooltip.replaceChild(newBrushDropdowndom, self._brushDropdownDom);
- self._brushDropdownDom = newBrushDropdowndom;
- }
- });
-
- const hasMarks = TooltipTextMenuManager.Instance._brushMarks && TooltipTextMenuManager.Instance._brushMarks.size > 0;
- return new Dropdown(hasMarks ? [brushInfo, clearBrush] : [brushInfo], { class: "buttonSettings-dropdown" }) as MenuItem;
- }
-
- static setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
- if (mark) {
- const node = (state.selection as NodeSelection).node;
- if (node?.type === schema.nodes.ordered_list) {
- let attrs = node.attrs;
- if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
- if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
- if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
- const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
- dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
- } else {
- toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
- const { from, $from, to, empty } = tx.selection;
- if (!tx.doc.rangeHasMark(from, to, mark.type)) {
- toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
- } else dispatch(tx);
- });
- }
- }
- }
-
- // called by Prosemirror
- update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); }
- //updates the tooltip menu when the selection changes
- public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
- if (!view) {
- console.log("no editor? why?");
- return;
- }
- this.view = view;
- DocumentDecorations.Instance.showTextBar();
- props && (this.editorProps = props);
-
- // Don't do anything if the document/selection didn't change
- if (!lastState || !lastState.doc.eq(view.state.doc) || !lastState.selection.eq(view.state.selection)) {
-
- // UPDATE LINK DROPDOWN
- const linkTarget = await this.getTextLinkTargetTitle()
- const linkDom = this.createLinkTool(linkTarget ? true : false).render(this.view).dom;
- const linkDropdownDom = this.createLinkDropdown(linkTarget).render(this.view).dom;
- this.linkDom && this.tooltip.replaceChild(linkDom, this.linkDom);
- this.linkDropdownDom && this.tooltip.replaceChild(linkDropdownDom, this.linkDropdownDom);
- this.linkDom = linkDom;
- this.linkDropdownDom = linkDropdownDom;
-
- //UPDATE FONT STYLE DROPDOWN
- const activeStyles = this.activeFontFamilyOnSelection();
- this.updateFontStyleDropdown(activeStyles.length === 1 ? activeStyles[0] : activeStyles.length ? "various" : "default");
-
- //UPDATE FONT SIZE DROPDOWN
- const activeSizes = this.activeFontSizeOnSelection();
- this.updateFontSizeDropdown(activeSizes.length === 1 ? String(activeSizes[0]) + " pt" : activeSizes.length ? "various" : "default");
-
- //UPDATE ALL OTHER BUTTONS
- this.updateHighlightStateOfButtons();
- }
- }
-
- updateHighlightStateOfButtons() {
- Array.from(this._marksToDoms.values()).forEach(val => val.style.fill = "white");
- this.activeMarksOnSelection().filter(mark => this._marksToDoms.has(mark)).forEach(mark =>
- this._marksToDoms.get(mark)!.style.fill = "greenyellow");
-
- // keeps brush tool highlighted if active when switching between textboxes
- if (!TooltipTextMenuManager.Instance._brushIsEmpty && this._brushdom) {
- const newbrush = this.createBrushTool(true).render(this.view).dom;
- this.tooltip.replaceChild(newbrush, this._brushdom);
- this._brushdom = newbrush;
- }
- }
-
- //finds fontSize at start of selection
- activeFontSizeOnSelection() {
- //current selection
- const state = this.view.state;
- const activeSizes: number[] = [];
- const pos = this.view.state.selection.$from;
- const ref_node: ProsNode = this.reference_node(pos);
- if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
- ref_node.marks.forEach(m => m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize));
- }
- return activeSizes;
- }
- //finds fontSize at start of selection
- activeFontFamilyOnSelection() {
- //current selection
- const state = this.view.state;
- const activeFamilies: string[] = [];
- const pos = this.view.state.selection.$from;
- const ref_node: ProsNode = this.reference_node(pos);
- if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
- ref_node.marks.forEach(m => m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family));
- }
- return activeFamilies;
- }
- //finds all active marks on selection in given group
- activeMarksOnSelection() {
- const markGroup = Array.from(this._marksToDoms.keys());
- if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
- //current selection
- const { empty, ranges, $to } = this.view.state.selection as TextSelection;
- const state = this.view.state;
- let activeMarks: MarkType[] = [];
- if (!empty) {
- activeMarks = markGroup.filter(mark => {
- const has = false;
- for (let i = 0; !has && i < ranges.length; i++) {
- return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
- }
- return false;
- });
- }
- else {
- const pos = this.view.state.selection.$from;
- const ref_node: ProsNode = this.reference_node(pos);
- if (ref_node !== null && ref_node !== this.view.state.doc) {
- if (ref_node.isText) {
- }
- else {
- return [];
- }
- activeMarks = markGroup.filter(mark_type => {
- if (mark_type === state.schema.marks.pFontSize) {
- return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
- }
- const mark = state.schema.mark(mark_type);
- return ref_node.marks.includes(mark);
- });
- }
- }
- return activeMarks;
- }
-
- reference_node(pos: ResolvedPos<any>): ProsNode {
- let ref_node: ProsNode = this.view.state.doc;
- if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
- ref_node = pos.nodeBefore;
- }
- else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
- ref_node = pos.nodeAfter;
- }
- else if (pos.pos > 0) {
- let skip = false;
- for (let i: number = pos.pos - 1; i > 0; i--) {
- this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
- if (node.isLeaf && !skip) {
- ref_node = node;
- skip = true;
- }
-
- });
- }
- }
- if (!ref_node.isLeaf && ref_node.childCount > 0) {
- ref_node = ref_node.child(0);
- }
- return ref_node;
- }
-
- destroy() {
- // this.wrapper.remove();
- }
-}
-
-
-export class TooltipTextMenuManager {
- private static _instance: TooltipTextMenuManager;
- private _isPinned: boolean = false;
-
- public pinnedX: number = 0;
- public pinnedY: number = 0;
- public unpinnedX: number = 0;
- public unpinnedY: number = 0;
-
- public _brushMarks: Set<Mark> | undefined;
- public _brushMap: Map<string, Set<Mark>> = new Map();
- public _brushIsEmpty: boolean = true;
-
- public color: String = "#000";
- public highlighter: String = "transparent";
-
- public activeMenu: TooltipTextMenu | undefined;
-
- static get Instance() {
- if (!TooltipTextMenuManager._instance) {
- TooltipTextMenuManager._instance = new TooltipTextMenuManager();
- }
- return TooltipTextMenuManager._instance;
- }
-
- public get isPinned() { return this._isPinned; }
-
- public toggleIsPinned() { this._isPinned = !this._isPinned; }
-}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js
index 27605d167..beb030635 100644
--- a/src/client/util/request-image-size.js
+++ b/src/client/util/request-image-size.js
@@ -38,7 +38,7 @@ module.exports = function requestImageSize(options) {
return reject(new HttpError(res.statusCode, res.statusMessage));
}
- let buffer = new Buffer([]);
+ let buffer = new Buffer.from([]);
let size;
let imageSizeError;
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 622e10960..127f7b798 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -131,6 +131,7 @@ interface Promise<T> {
declare const Update: unique symbol;
declare const Self: unique symbol;
declare const SelfProxy: unique symbol;
+declare const DataSym: unique symbol;
declare const HandleUpdate: unique symbol;
declare const Id: unique symbol;
declare const OnUpdate: unique symbol;
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index f3da5f284..d4a76ee17 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -5,13 +5,26 @@
background: #323232;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
border-radius: 0px 6px 6px 6px;
- overflow: hidden;
+ // overflow: hidden;
display: flex;
+ &.with-rows {
+ flex-direction: column
+ }
+
+ .antimodeMenu-row {
+ display: flex;
+ height: 35px;
+ }
+
.antimodeMenu-button {
background-color: transparent;
width: 35px;
height: 35px;
+
+ &.active {
+ background-color: #121212;
+ }
}
.antimodeMenu-button:hover {
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 408df8bc2..4625eb92f 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -18,9 +18,13 @@ export default abstract class AntimodeMenu extends React.Component {
@observable protected _opacity: number = 1;
@observable protected _transition: string = "opacity 0.5s";
@observable protected _transitionDelay: string = "";
+ @observable protected _canFade: boolean = true;
@observable public Pinned: boolean = false;
+ get width() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0; }
+ get height() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0; }
+
@action
/**
* @param x
@@ -62,7 +66,7 @@ export default abstract class AntimodeMenu extends React.Component {
@action
protected pointerLeave = (e: React.PointerEvent) => {
- if (!this.Pinned) {
+ if (!this.Pinned && this._canFade) {
this._transition = "opacity 0.5s";
this._transitionDelay = "1s";
this._opacity = 0.2;
@@ -88,8 +92,8 @@ export default abstract class AntimodeMenu extends React.Component {
document.removeEventListener("pointerup", this.dragEnd);
document.addEventListener("pointerup", this.dragEnd);
- this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
- this._offsetY = e.nativeEvent.offsetY;
+ this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left;
+ this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top;
e.stopPropagation();
e.preventDefault();
@@ -97,8 +101,14 @@ export default abstract class AntimodeMenu extends React.Component {
@action
protected dragging = (e: PointerEvent) => {
- this._left = e.pageX - this._offsetX;
- this._top = e.pageY - this._offsetY;
+ const width = this._mainCont.current!.getBoundingClientRect().width;
+ const height = this._mainCont.current!.getBoundingClientRect().height;
+
+ const left = e.pageX - this._offsetX;
+ const top = e.pageY - this._offsetY;
+
+ this._left = Math.min(Math.max(left, 0), window.innerWidth - width);
+ this._top = Math.min(Math.max(top, 0), window.innerHeight - height);
e.stopPropagation();
e.preventDefault();
@@ -116,6 +126,10 @@ export default abstract class AntimodeMenu extends React.Component {
e.preventDefault();
}
+ protected getDragger = () => {
+ return <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />;
+ }
+
protected getElement(buttons: JSX.Element[]) {
return (
<div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
@@ -125,4 +139,14 @@ export default abstract class AntimodeMenu extends React.Component {
</div>
);
}
+
+ protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
+ return (
+ <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
+ {rows}
+ {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>}
+ </div>
+ );
+ }
} \ No newline at end of file
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 937aff0d6..ac803d977 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -77,8 +77,13 @@ export class ContextMenu extends React.Component {
@action
clearItems() {
this._items = [];
+ this._defaultPrefix = "";
+ this._defaultItem = undefined;
}
+ _defaultPrefix: string = "";
+ _defaultItem: ((name: string) => void) | undefined;
+
findByDescription = (target: string, toLowerCase = false) => {
return this._items.find(menuItem => {
let reference = menuItem.description;
@@ -93,6 +98,11 @@ export class ContextMenu extends React.Component {
this._items.push(item);
}
}
+ @action
+ setDefaultItem(prefix: string, item: (name: string) => void) {
+ this._defaultPrefix = prefix;
+ this._defaultItem = item;
+ }
getItems() {
return this._items;
@@ -248,7 +258,11 @@ export class ContextMenu extends React.Component {
e.preventDefault();
} else if (e.key === "Enter" || e.key === "Tab") {
const item = this.flatItems[this.selectedIndex];
- item && item.event({ x: this.pageX, y: this.pageY });
+ if (item) {
+ item.event({ x: this.pageX, y: this.pageY });
+ } else if (this._searchString.startsWith(this._defaultPrefix)) {
+ this._defaultItem?.(this._searchString.substring(this._defaultPrefix.length));
+ }
this.closeMenu();
e.preventDefault();
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 4dbf26956..ce48e1215 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -34,8 +34,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document(): T { return schemaCtor(this.props.Document); }
@computed get layoutDoc() { return Doc.Layout(this.props.Document); }
- @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; }
active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools
}
return Component;
@@ -43,7 +42,7 @@ export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCt
/// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
-interface DocAnnotatableProps {
+export interface DocAnnotatableProps {
Document: Doc;
DataDoc?: Doc;
fieldKey: string;
@@ -58,15 +57,16 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document(): T { return schemaCtor(this.props.Document); }
@computed get layoutDoc() { return Doc.Layout(this.props.Document); }
- @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
- @computed get extensionDocSync() { return Doc.fieldExtensionDocSync(this.dataDoc, this.props.fieldKey); }
- @computed get annotationsKey() { return "annotations"; }
+ @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; }
+
+ _annotationKey: string = "annotations";
+ public set annotationKey(val: string) { this._annotationKey = val; }
+ public get annotationKey() { return this._annotationKey; }
@action.bound
removeDocument(doc: Doc): boolean {
Doc.GetProto(doc).annotationOn = undefined;
- const value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
+ const value = Cast(this.dataDoc[this.props.fieldKey + "-" + this._annotationKey], listSpec(Doc), []);
const index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
return index !== -1 && value && value.splice(index, 1) ? true : false;
}
@@ -79,7 +79,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
@action.bound
addDocument(doc: Doc): boolean {
Doc.GetProto(doc).annotationOn = this.props.Document;
- return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
+ return Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-" + this._annotationKey, doc) ? true : false;
}
whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 202bfe400..65d1ade2a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,26 +1,28 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, runInAction, computed } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Id } from '../../new_fields/FieldSymbols';
import { RichTextField } from '../../new_fields/RichTextField';
-import { NumCast, StrCast, Cast } from "../../new_fields/Types";
+import { NumCast, StrCast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
-import { DragManager } from "../util/DragManager";
+import RichTextMenu from '../util/RichTextMenu';
import { UndoManager } from "../util/UndoManager";
-import './DocumentButtonBar.scss';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+import { ParentDocSelector } from './collections/ParentDocumentSelector';
import './collections/ParentDocumentSelector.scss';
+import './DocumentButtonBar.scss';
import { LinkMenu } from "./linking/LinkMenu";
-import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
+import { DocumentView } from './nodes/DocumentView';
+import { GoogleRef } from "./nodes/FormattedTextBox";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
-import { DocumentView } from './nodes/DocumentView';
-import { ParentDocSelector } from './collections/ParentDocumentSelector';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import { Id } from '../../new_fields/FieldSymbols';
+import { DragManager } from '../util/DragManager';
+import { MetadataEntryMenu } from './MetadataEntryMenu';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -42,6 +44,7 @@ const fetch: IconProp = "sync-alt";
@observer
export class DocumentButtonBar extends React.Component<{ views: (DocumentView | undefined)[], stack?: any }, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
+ private _dragRef = React.createRef<HTMLDivElement>();
private _downX = 0;
private _downY = 0;
private _pullAnimating = false;
@@ -112,14 +115,15 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
const linkDrag = UndoManager.StartBatch("Drag Link");
this.view0 && DragManager.StartLinkDrag(this._linkButton.current, this.view0.props.Document, e.pageX, e.pageY, {
dragComplete: dropEv => {
- const linkDoc = dropEv.linkDragData?.linkDocument; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
- if (this.view0 && linkDoc && FormattedTextBox.ToolTipTextMenu) {
+ const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
+ if (this.view0 && linkDoc) {
const proto = Doc.GetProto(linkDoc);
proto.sourceContext = this.view0.props.ContainingCollectionDoc;
const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
+ const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
+ const text = RichTextMenu.Instance.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", anchor2Id);
if (linkDoc.anchor2 instanceof Doc) {
- const text = FormattedTextBox.ToolTipTextMenu.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", linkDoc.anchor2[Id]);
proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODO open to more descriptive descriptions of following in text link
}
}
@@ -199,7 +203,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
const view0 = this.view0;
const linkCount = view0 && DocListCast(view0.props.Document.links).length;
return !view0 ? (null) : <div title="Drag(create link) Tap(view links)" className="documentButtonBar-linkFlyout" ref={this._linkButton}>
- <Flyout anchorPoint={anchorPoints.RIGHT_TOP}
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
content={<LinkMenu docView={view0} addDocTab={view0.props.addDocTab} changeFlyout={emptyFunction} />}>
<div className={"documentButtonBar-linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >
{linkCount ? linkCount : <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />}
@@ -209,6 +213,19 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
}
@computed
+ get metadataButton() {
+ const view0 = this.view0;
+ return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout" ref={this._linkButton}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
+ content={<MetadataEntryMenu docs={() => this.props.views.filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
+ <div className={"documentButtonBar-linkButton-" + "empty"} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
+ </div>
+ </Flyout>
+ </div>;
+ }
+
+ @computed
get contextButton() {
return !this.view0 ? (null) : <ParentDocSelector Views={this.props.views.filter(v => v).map(v => v as DocumentView)} Document={this.view0.props.Document} addDocTab={(doc, data, where) => {
where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) :
@@ -218,11 +235,61 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
}} />;
}
- render() {
- if (!this.view0) return (null);
+ private _downx = 0;
+ private _downy = 0;
+ onAliasButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
+ e.stopPropagation();
+ }
+
+ onAliasButtonDown = (e: React.PointerEvent): void => {
+ this._downx = e.clientX;
+ this._downy = e.clientY;
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.addEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
+ document.addEventListener("pointerup", this.onAliasButtonUp);
+ }
+ onAliasButtonMoved = (e: PointerEvent): void => {
+ if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
+
+ const dragDocView = this.props.views[0]!;
+ const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ dragData.embedDoc = true;
+ dragData.dropAction = "alias";
+ DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, {
+ offsetX: dragData.offset[0],
+ offsetY: dragData.offset[1],
+ hideSource: false
+ });
+ }
+ e.stopPropagation();
+ }
+
+ @computed
+ get templateButton() {
+ const view0 = this.view0;
const templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template =>
templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
+ return !view0 ? (null) : <div title="Customize layout" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP}
+ content={<TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
+ <div className={"documentButtonBar-linkButton-" + "empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
+ {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ </div>
+ </Flyout>
+ </div>;
+ }
+
+ render() {
+ if (!this.view0) return (null);
const isText = this.view0.props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field. need to add fieldKey to DocumentView
const considerPull = isText && this.considerGoogleDocsPull;
@@ -232,7 +299,13 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
{this.linkButton}
</div>
<div className="documentButtonBar-button">
- <TemplateMenu docs={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />
+ {this.templateButton}
+ </div>
+ <div className="documentButtonBar-button">
+ {this.metadataButton}
+ </div>
+ <div className="documentButtonBar-button">
+ {this.contextButton}
</div>
<div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}>
{this.considerGoogleDocsPush}
@@ -240,7 +313,6 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
<div className="documentButtonBar-button" style={{ display: !considerPull ? "none" : "" }}>
{this.considerGoogleDocsPull}
</div>
- {this.contextButton}
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4bc24fa93..c5034b901 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,31 +1,25 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { Doc } from "../../new_fields/Doc";
import { PositionDocument } from '../../new_fields/documentSchemas';
-import { List } from "../../new_fields/List";
import { ObjectField } from '../../new_fields/ObjectField';
-import { Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { ScriptField } from '../../new_fields/ScriptField';
+import { Cast, StrCast } from "../../new_fields/Types";
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { Utils } from "../../Utils";
-import { Docs, DocUtils } from "../documents/Documents";
-import { DocumentManager } from "../util/DocumentManager";
+import { DocUtils } from "../documents/Documents";
+import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
-import { TooltipTextMenu } from '../util/TooltipTextMenu';
import { undoBatch, UndoManager } from "../util/UndoManager";
-import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { CollectionView } from "./collections/CollectionView";
import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
import { DocumentView } from "./nodes/DocumentView";
-import { FieldView } from "./nodes/FieldView";
import { IconBox } from "./nodes/IconBox";
import React = require("react");
-import { DocumentType } from '../documents/DocumentTypes';
-import { ScriptField } from '../../new_fields/ScriptField';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -52,17 +46,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _titleHeight = 20;
private _downX = 0;
private _downY = 0;
- private _iconDoc?: Doc = undefined;
private _resizeUndo?: UndoManager.Batch;
private _radiusDown = [0, 0];
@observable private _accumulatedTitle = "";
- @observable private _minimizedX = 0;
- @observable private _minimizedY = 0;
@observable private _titleControlString: string = "#title";
@observable private _edtingTitle = false;
@observable private _hidden = false;
@observable private _opacity = 1;
- @observable private _removeIcon = false;
@observable public Interacting = false;
@observable public pushIcon: IconProp = "arrow-alt-circle-up";
@@ -246,15 +236,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@action
onMinimizeDown = (e: React.PointerEvent): void => {
e.stopPropagation();
- this._iconDoc = undefined;
if (e.button === 0) {
- this._downX = e.pageX;
- this._downY = e.pageY;
- this._removeIcon = false;
- const selDoc = SelectionManager.SelectedDocuments()[0];
- const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
- this._minimizedX = selDocPos[0] + 12;
- this._minimizedY = selDocPos[1] + 12;
document.removeEventListener("pointermove", this.onMinimizeMove);
document.addEventListener("pointermove", this.onMinimizeMove);
document.removeEventListener("pointerup", this.onMinimizeUp);
@@ -267,20 +249,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.stopPropagation();
if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD ||
Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) {
- const selDoc = SelectionManager.SelectedDocuments()[0];
- const selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
- const snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20;
- this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX;
- this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY;
- const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
-
- if (selectedDocs.length > 1) {
- this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
- this.moveIconDoc(this._iconDoc);
- } else {
- this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
- }
- this._removeIcon = snapped;
+ document.removeEventListener("pointermove", this.onMinimizeMove);
+ document.removeEventListener("pointerup", this.onMinimizeUp);
}
}
@undoBatch
@@ -291,59 +261,19 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.removeEventListener("pointermove", this.onMinimizeMove);
document.removeEventListener("pointerup", this.onMinimizeUp);
const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
- if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
- selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
- }
- if (!this._removeIcon && selectedDocs.length === 1) { // if you click on the top-left button when just 1 doc is selected, then collapse it. not sure why we don't do it for multiple selections
- this.getIconDoc(selectedDocs[0]).then(async icon => {
- const minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc);
- if (minimizedDoc) {
- const scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint(
- NumCast(minimizedDoc.x) - NumCast(selectedDocs[0].Document.x), NumCast(minimizedDoc.y) - NumCast(selectedDocs[0].Document.y));
- SelectionManager.DeselectAll();
- DocumentManager.Instance.animateBetweenPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
- }
- });
- }
- this._removeIcon = false;
- }
- runInAction(() => this._minimizedX = this._minimizedY = 0);
- }
-
- @undoBatch
- @action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
- const doc = selected[0].props.Document;
- const iconDoc = Docs.Create.IconDocument(layoutString);
- iconDoc.isButton = true;
-
- IconBox.AutomaticTitle(iconDoc);
- //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
- iconDoc.width = Number(MINIMIZED_ICON_SIZE);
- iconDoc.height = Number(MINIMIZED_ICON_SIZE);
- iconDoc.x = NumCast(doc.x);
- iconDoc.y = NumCast(doc.y) - 24;
- iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
- selected.length === 1 && (doc.minimizedDoc = iconDoc);
- selected[0].props.addDocument && selected[0].props.addDocument(iconDoc);
- return iconDoc;
- }
- @action
- public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
- const doc = docView.props.Document;
- let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
-
- if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
- const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, ""));
- iconDoc = this.createIcon([docView], layout);
+ selectedDocs.map(dv => {
+ const layoutKey = Cast(dv.props.Document.layoutKey, "string", null);
+ const collapse = layoutKey !== "layout_icon";
+ if (collapse) {
+ if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
+ dv.setCustomView(collapse, "icon");
+ } else {
+ const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null);
+ dv.setCustomView(deiconifyLayout ? true : false, deiconifyLayout);
+ dv.props.Document.deiconifyLayout = undefined;
+ }
+ });
}
- return iconDoc;
- }
- moveIconDoc(iconDoc: Doc) {
- const selView = SelectionManager.SelectedDocuments()[0];
- const where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).
- transformPoint(this._minimizedX - 12, this._minimizedY - 12);
- iconDoc.x = where[0] + NumCast(selView.props.Document.x);
- iconDoc.y = where[1] + NumCast(selView.props.Document.y);
}
@action
@@ -363,14 +293,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onRadiusMove = (e: PointerEvent): void => {
let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
dist = dist < 3 ? 0 : dist;
- let usingRule = false;
- SelectionManager.SelectedDocuments().map(dv => {
- const ruleProvider = dv.props.ruleProvider;
- const heading = NumCast(dv.props.Document.heading);
- ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
- usingRule = usingRule || (ruleProvider && heading ? true : false);
- });
- !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateForField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
map(d => d.borderRounding = `${Math.min(100, dist)}%`);
e.stopPropagation();
e.preventDefault();
@@ -462,10 +385,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
const doc = PositionDocument(element.props.Document);
const layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
- let nwidth = layoutDoc.nativeWidth || 0;
- let nheight = layoutDoc.nativeHeight || 0;
- const width = (layoutDoc.width || 0);
- const height = (layoutDoc.height || (nheight / nwidth * width));
+ let nwidth = layoutDoc._nativeWidth || 0;
+ let nheight = layoutDoc._nativeHeight || 0;
+ const width = (layoutDoc._width || 0);
+ const height = (layoutDoc._height || (nheight / nwidth * width));
const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
const actualdW = Math.max(width + (dW * scale), 20);
const actualdH = Math.max(height + (dH * scale), 20);
@@ -474,34 +397,34 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
layoutDoc.ignoreAspect = false;
- layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
- layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+ layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+ layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
if (fixedAspect && (!nwidth || !nheight)) {
- layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
- layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+ layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+ layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
+ layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
}
- layoutDoc.width = actualdW;
- if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
- else layoutDoc.height = actualdH;
+ layoutDoc._width = actualdW;
+ if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
+ else layoutDoc._height = actualdH;
}
else {
if (!fixedAspect) {
- layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
+ layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
}
- layoutDoc.height = actualdH;
- if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
- else layoutDoc.width = actualdW;
+ layoutDoc._height = actualdH;
+ if (fixedAspect && !layoutDoc._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);
+ dW && (layoutDoc._width = actualdW);
+ dH && (layoutDoc._height = actualdH);
+ dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
}
}
}));
@@ -544,11 +467,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.TextBar = ele;
}
}
- public showTextBar = () => {
- if (this.TextBar && TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1) {
- this.TextBar.appendChild(TooltipTextMenu.Toolbar);
- }
- }
render() {
const bounds = this.Bounds;
const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 54def38b5..780c5b2f4 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,10 +1,12 @@
import React = require('react');
+import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { observable, action, trace } from 'mobx';
-import "./EditableView.scss";
import * as Autosuggest from 'react-autosuggest';
-import { undoBatch } from '../util/UndoManager';
+import { ObjectField } from '../../new_fields/ObjectField';
import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField';
+import { ContextMenu } from './ContextMenu';
+import { ContextMenuProps } from './ContextMenuItem';
+import "./EditableView.scss";
export interface EditableProps {
/**
@@ -36,13 +38,14 @@ export interface EditableProps {
resetValue: () => void;
value: string,
onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void,
- autosuggestProps: Autosuggest.AutosuggestProps<string>
+ autosuggestProps: Autosuggest.AutosuggestProps<string, any>
};
oneLine?: boolean;
editing?: boolean;
onClick?: (e: React.MouseEvent) => boolean;
isEditingCallback?: (isEditing: boolean) => void;
+ menuCallback?: (x: number, y: number) => void;
HeadingObject?: SchemaHeaderField | undefined;
HeadingsHack?: number;
toggle?: () => void;
@@ -65,7 +68,7 @@ export class EditableView extends React.Component<EditableProps> {
}
@action
- componentWillReceiveProps(nextProps: EditableProps) {
+ componentDidUpdate(nextProps: EditableProps) {
// this is done because when autosuggest is turned on, the suggestions are passed in as a prop,
// so when the suggestions are passed in, and no editing prop is passed in, it used to set it
// to false. this will no longer do so -syip
@@ -87,12 +90,14 @@ export class EditableView extends React.Component<EditableProps> {
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
this._editing = false;
- this.props.isEditingCallback && this.props.isEditingCallback(false);
+ this.props.isEditingCallback?.(false);
}
} else if (e.key === "Escape") {
e.stopPropagation();
this._editing = false;
- this.props.isEditingCallback && this.props.isEditingCallback(false);
+ this.props.isEditingCallback?.(false);
+ } else if (e.key === ":") {
+ this.props.menuCallback?.(e.currentTarget.offsetLeft, e.currentTarget.offsetTop);
}
}
@@ -101,7 +106,7 @@ export class EditableView extends React.Component<EditableProps> {
e.nativeEvent.stopPropagation();
if (!this.props.onClick || !this.props.onClick(e)) {
this._editing = true;
- this.props.isEditingCallback && this.props.isEditingCallback(true);
+ this.props.isEditingCallback?.(true);
}
e.stopPropagation();
}
@@ -110,7 +115,7 @@ export class EditableView extends React.Component<EditableProps> {
private finalizeEdit(value: string, shiftDown: boolean) {
this._editing = false;
if (this.props.SetValue(value, shiftDown)) {
- this.props.isEditingCallback && this.props.isEditingCallback(false);
+ this.props.isEditingCallback?.(false);
}
}
@@ -152,7 +157,7 @@ export class EditableView extends React.Component<EditableProps> {
/>;
} else {
if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
- return (
+ return (this.props.contents instanceof ObjectField ? (null) :
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }}
onClick={this.onClick}>
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index 31601efd4..2fad87ee0 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -5,4 +5,45 @@
top: 0;
left: 0;
touch-action: none;
+}
+
+.clipboardDoc-cont {
+ position: absolute;
+ width: 300px;
+ height: 300px;
+}
+
+.inkToTextDoc-cont {
+ position: absolute;
+ width: 300px;
+ overflow: hidden;
+ pointer-events: none;
+
+ .inkToTextDoc-scroller {
+ overflow: visible;
+ position: absolute;
+ width: 100%;
+
+ .menuItem-cont {
+ width: 100%;
+ height: 20px;
+ padding: 2.5px;
+ border-bottom: .5px solid black;
+ }
+ }
+
+ .shadow {
+ width: 100%;
+ height: calc(100% - 25px);
+ position: absolute;
+ top: 25px;
+ background-color: black;
+ opacity: 0.2;
+ }
+}
+
+.filter-cont {
+ position: absolute;
+ background-color: transparent;
+ border: 1px solid black;
} \ No newline at end of file
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 029834725..afae61630 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,38 +2,64 @@ import React = require("react");
import { Touchable } from "./Touchable";
import { observer } from "mobx-react";
import "./GestureOverlay.scss";
-import { computed, observable, action, runInAction } from "mobx";
-import { CreatePolyline } from "./InkingStroke";
+import { computed, observable, action, runInAction, IReactionDisposer, reaction, flow, trace } from "mobx";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
import { InteractionUtils } from "../util/InteractionUtils";
import { InkingControl } from "./InkingControl";
-import { InkTool } from "../../new_fields/InkField";
+import { InkTool, InkData } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
import { LinkManager } from "../util/LinkManager";
-import { DocUtils } from "../documents/Documents";
+import { DocUtils, Docs } from "../documents/Documents";
import { undoBatch } from "../util/UndoManager";
import { Scripting } from "../util/Scripting";
-import { FieldValue, Cast, NumCast } from "../../new_fields/Types";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-import Palette from "./Palette";
-import { Utils } from "../../Utils";
+import HorizontalPalette from "./Palette";
+import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange } from "../../Utils";
+import { DocumentView } from "./nodes/DocumentView";
+import { Transform } from "../util/Transform";
+import { DocumentContentsView } from "./nodes/DocumentContentsView";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { DocServer } from "../DocServer";
+import htmlToImage from "html-to-image";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { CollectionViewType } from "./collections/CollectionView";
+import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
@observer
export default class GestureOverlay extends Touchable {
static Instance: GestureOverlay;
- @observable private _points: { X: number, Y: number }[] = [];
- @observable private _palette?: JSX.Element;
@observable public Color: string = "rgb(244, 67, 54)";
@observable public Width: number = 5;
+ @observable public SavedColor?: string;
+ @observable public SavedWidth?: number;
+ @observable public Tool: ToolglassTools = ToolglassTools.None;
+
+ @observable private _thumbX?: number;
+ @observable private _thumbY?: number;
+ @observable private _selectedIndex: number = -1;
+ @observable private _menuX: number = -300;
+ @observable private _menuY: number = -300;
+ @observable private _pointerY?: number;
+ @observable private _points: { X: number, Y: number }[] = [];
+ @observable private _strokes: InkData[] = [];
+ @observable private _palette?: JSX.Element;
+ @observable private _clipboardDoc?: JSX.Element;
+ @observable private _possibilities: JSX.Element[] = [];
+
+ @computed private get height(): number { return Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); }
+ @computed private get showBounds() { return this.Tool !== ToolglassTools.None; }
private _d1: Doc | undefined;
+ private _inkToTextDoc: Doc | undefined;
private _thumbDoc: Doc | undefined;
- private _thumbX?: number;
- private _thumbY?: number;
private thumbIdentifier?: number;
+ private pointerIdentifier?: number;
+ private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
private _holdTimer: NodeJS.Timeout | undefined;
- private _hands: (React.Touch[])[] = [];
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -43,6 +69,11 @@ export default class GestureOverlay extends Touchable {
GestureOverlay.Instance = this;
}
+ componentDidMount = () => {
+ this._thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
+ }
+
getNewTouches(e: React.TouchEvent | TouchEvent) {
const ntt: (React.Touch | Touch)[] = Array.from(e.targetTouches);
const nct: (React.Touch | Touch)[] = Array.from(e.changedTouches);
@@ -50,22 +81,22 @@ export default class GestureOverlay extends Touchable {
this._hands.forEach((hand) => {
for (let i = 0; i < e.targetTouches.length; i++) {
const pt = e.targetTouches.item(i);
- if (pt && hand.some((finger) => finger.screenX === pt ?.screenX && finger.screenY === pt.screenY)) {
- ntt.splice(ntt.indexOf(pt));
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ ntt.splice(ntt.indexOf(pt), 1);
}
}
for (let i = 0; i < e.changedTouches.length; i++) {
const pt = e.changedTouches.item(i);
- if (pt && hand.some((finger) => finger.screenX === pt ?.screenX && finger.screenY === pt.screenY)) {
- nct.splice(nct.indexOf(pt));
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ nct.splice(nct.indexOf(pt), 1);
}
}
for (let i = 0; i < e.touches.length; i++) {
const pt = e.touches.item(i);
- if (pt && hand.some((finger) => finger.screenX === pt ?.screenX && finger.screenY === pt.screenY)) {
- nt.splice(ntt.indexOf(pt));
+ if (pt && hand.some((finger) => finger.screenX === pt.screenX && finger.screenY === pt.screenY)) {
+ nt.splice(nt.indexOf(pt), 1);
}
}
});
@@ -95,10 +126,9 @@ export default class GestureOverlay extends Touchable {
});
ptsToDelete.forEach(pt => this.prevPoints.delete(pt));
+ const nts = this.getNewTouches(te);
- if (this.prevPoints.size && this.prevPoints.size < 5) {
-
- const nts = this.getNewTouches(te);
+ if (nts.nt.length < 5) {
const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
target ?.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
@@ -267,7 +297,7 @@ export default class GestureOverlay extends Touchable {
e.stopPropagation();
}
- handleHandDown = (e: React.TouchEvent) => {
+ handleHandDown = async (e: React.TouchEvent) => {
const fingers = new Array<React.Touch>();
for (let i = 0; i < e.touches.length; i++) {
const pt: any = e.touches.item(i);
@@ -283,27 +313,47 @@ export default class GestureOverlay extends Touchable {
}
}
const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
- if (thumb.identifier === this.thumbIdentifier) {
- this._thumbX = thumb.clientX;
- this._thumbY = thumb.clientY;
- return;
+ const rightMost = Math.max(...fingers.map(f => f.clientX));
+ const leftMost = Math.min(...fingers.map(f => f.clientX));
+ let pointer: React.Touch | undefined;
+ // left hand
+ if (thumb.clientX === rightMost) {
+ pointer = fingers.reduce((a, v) => a.clientX > v.clientX || v.identifier === thumb.identifier ? a : v);
+ }
+ // right hand
+ else if (thumb.clientX === leftMost) {
+ pointer = fingers.reduce((a, v) => a.clientX < v.clientX || v.identifier === thumb.identifier ? a : v);
+ }
+ else {
+ console.log("not hand");
}
- this.thumbIdentifier = thumb ?.identifier;
- fingers.forEach((f) => this.prevPoints.delete(f.identifier));
- this._hands.push(fingers);
+ this.pointerIdentifier = pointer?.identifier;
+ runInAction(() => {
+ this._pointerY = pointer?.clientY;
+ if (thumb.identifier === this.thumbIdentifier) {
+ this._thumbX = thumb.clientX;
+ this._thumbY = thumb.clientY;
+ this._hands.set(thumb.identifier, fingers);
+ return;
+ }
+ });
+
+ this.thumbIdentifier = thumb?.identifier;
+ this._hands.set(thumb.identifier, fingers);
const others = fingers.filter(f => f !== thumb);
const minX = Math.min(...others.map(f => f.clientX));
const minY = Math.min(...others.map(f => f.clientY));
- // const t = this.getTransform().transformPoint(minX, minY);
- // const th = this.getTransform().transformPoint(thumb.clientX, thumb.clientY);
- const thumbDoc = FieldValue(Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc));
+ const thumbDoc = await Cast(CurrentUserUtils.setupThumbDoc(CurrentUserUtils.UserDocument), Doc);
if (thumbDoc) {
runInAction(() => {
+ this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc));
this._thumbDoc = thumbDoc;
this._thumbX = thumb.clientX;
this._thumbY = thumb.clientY;
- this._palette = <Palette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
+ this._menuX = thumb.clientX + 50;
+ this._menuY = thumb.clientY;
+ this._palette = <HorizontalPalette x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
});
}
@@ -316,14 +366,47 @@ export default class GestureOverlay extends Touchable {
@action
handleHandMove = (e: TouchEvent) => {
+ const fingers = new Array<React.Touch>();
+ for (let i = 0; i < e.touches.length; i++) {
+ const pt: any = e.touches.item(i);
+ if (pt.radiusX > 1 && pt.radiusY > 1) {
+ for (let j = 0; j < e.targetTouches.length; j++) {
+ const tPt = e.targetTouches.item(j);
+ if (tPt?.screenX === pt?.screenX && tPt?.screenY === pt?.screenY) {
+ if (pt && this.prevPoints.has(pt.identifier)) {
+ this._hands.forEach(hand => hand.some(f => {
+ if (f.identifier === pt.identifier) {
+ fingers.push(pt);
+ }
+ }));
+ }
+ }
+ }
+ }
+ }
+ const thumb = fingers.reduce((a, v) => a.clientY > v.clientY ? a : v, fingers[0]);
+ if (thumb?.identifier && thumb?.identifier === this.thumbIdentifier) {
+ this._hands.set(thumb.identifier, fingers);
+ }
+
for (let i = 0; i < e.changedTouches.length; i++) {
const pt = e.changedTouches.item(i);
- if (pt && pt.identifier === this.thumbIdentifier && this._thumbX && this._thumbDoc) {
- if (Math.abs(pt.clientX - this._thumbX) > 20) {
- this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
- this._thumbX = pt.clientX;
+ if (pt && pt.identifier === this.thumbIdentifier && this._thumbY) {
+ if (this._thumbX && this._thumbDoc) {
+ if (Math.abs(pt.clientX - this._thumbX) > 30) {
+ this._thumbDoc.selectedIndex = Math.max(0, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
+ this._thumbX = pt.clientX;
+ }
+ }
+ if (this._thumbY && this._inkToTextDoc) {
+ if (Math.abs(pt.clientY - this._thumbY) > 20) {
+ this._selectedIndex = Math.max(0, -Math.floor((pt.clientY - this._thumbY) / 20));
+ }
}
}
+ if (pt && pt.identifier === this.pointerIdentifier) {
+ this._pointerY = pt.clientY;
+ }
}
}
@@ -331,9 +414,28 @@ export default class GestureOverlay extends Touchable {
handleHandUp = (e: TouchEvent) => {
if (e.touches.length < 3) {
// this.onTouchEnd(e);
+ if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier);
this._palette = undefined;
this.thumbIdentifier = undefined;
this._thumbDoc = undefined;
+
+ let scriptWorked = false;
+ if (NumCast(this._inkToTextDoc?.selectedIndex) > -1) {
+ const selectedButton = this._possibilities[NumCast(this._inkToTextDoc?.selectedIndex)];
+ if (selectedButton) {
+ selectedButton.props.onClick();
+ scriptWorked = true;
+ }
+ }
+
+ if (!scriptWorked) {
+ this._strokes.forEach(s => {
+ this.dispatchGesture(GestureUtils.Gestures.Stroke, s);
+ });
+ }
+ this._strokes = [];
+ this._points = [];
+ this._possibilities = [];
document.removeEventListener("touchend", this.handleHandUp);
}
}
@@ -358,6 +460,22 @@ export default class GestureOverlay extends Touchable {
this._points.push({ X: e.clientX, Y: e.clientY });
e.stopPropagation();
e.preventDefault();
+
+
+ if (this._points.length > 1) {
+ const B = this.svgBounds;
+ const initialPoint = this._points[0.];
+ const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
+ const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+ if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
+ switch (this.Tool) {
+ case ToolglassTools.RadialMenu:
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ //this.handle1PointerHoldStart(e);
+ }
+ }
+ }
}
}
@@ -393,67 +511,97 @@ export default class GestureOverlay extends Touchable {
return actionPerformed;
}
-
-
@action
onPointerUp = (e: PointerEvent) => {
if (this._points.length > 1) {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
- const result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
- let actionPerformed = false;
- if (result && result.Score > 0.7) {
- switch (result.Name) {
- case GestureUtils.Gestures.Box:
- const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
- target ?.dispatchEvent(new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
- {
- bubbles: true,
- detail: {
- points: this._points,
- gesture: GestureUtils.Gestures.Box,
- bounds: B
- }
- }));
- actionPerformed = true;
- break;
- case GestureUtils.Gestures.Line:
- actionPerformed = this.handleLineGesture();
+ const initialPoint = this._points[0.];
+ const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height;
+ const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - this.height && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
+
+ if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
+ switch (this.Tool) {
+ case ToolglassTools.InkToText:
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this._strokes.push(new Array(...this._points));
+ this._points = [];
+ CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then((results) => {
+ const wordResults = results.filter((r: any) => r.category === "inkWord" || r.category === "paragraph");
+ const possibilities: string[] = [];
+ if (wordResults[0]?.recognizedText) {
+ possibilities.push(wordResults[0]?.recognizedText)
+ }
+ possibilities.push(...wordResults[0]?.alternates?.map((a: any) => a.recognizedString));
+ console.log(possibilities);
+ const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right));
+ const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left));
+ const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top));
+ runInAction(() => {
+ this._possibilities = possibilities.map(p =>
+ <TouchScrollableMenuItem text={p} onClick={() => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Text, [{ X: l, Y: t }], p)} />);
+ });
+ });
break;
- case GestureUtils.Gestures.Scribble:
- console.log("scribble");
+ case ToolglassTools.IgnoreGesture:
+ this.dispatchGesture(GestureUtils.Gestures.Stroke);
+ this._points = [];
break;
}
- if (actionPerformed) {
- this._points = [];
- }
}
+ else {
+ const result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
+ let actionPerformed = false;
+ if (result && result.Score > 0.7) {
+ switch (result.Name) {
+ case GestureUtils.Gestures.Box:
+ this.dispatchGesture(GestureUtils.Gestures.Box);
+ actionPerformed = true;
+ break;
+ case GestureUtils.Gestures.Line:
+ actionPerformed = this.handleLineGesture();
+ break;
+ case GestureUtils.Gestures.Scribble:
+ console.log("scribble");
+ break;
+ }
+ if (actionPerformed) {
+ this._points = [];
+ }
+ }
- if (!actionPerformed) {
- const target = document.elementFromPoint(this._points[0].X, this._points[0].Y);
- target ?.dispatchEvent(
- new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
- {
- bubbles: true,
- detail: {
- points: this._points,
- gesture: GestureUtils.Gestures.Stroke,
- bounds: B
- }
- }
- )
- );
- this._points = [];
+ if (!actionPerformed) {
+ this.dispatchGesture(GestureUtils.Gestures.Stroke);
+ this._points = [];
+ }
}
}
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
}
- @computed get svgBounds() {
- const xs = this._points.map(p => p.X);
- const ys = this._points.map(p => p.Y);
+ dispatchGesture = (gesture: GestureUtils.Gestures, stroke?: InkData, data?: any) => {
+ const target = document.elementFromPoint((stroke ?? this._points)[0].X, (stroke ?? this._points)[0].Y);
+ target?.dispatchEvent(
+ new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: stroke ?? this._points,
+ gesture: gesture,
+ bounds: this.getBounds(stroke ?? this._points),
+ text: data
+ }
+ }
+ )
+ );
+ }
+
+ getBounds = (stroke: InkData) => {
+ const xs = stroke.map(p => p.X);
+ const ys = stroke.map(p => p.Y);
const right = Math.max(...xs);
const left = Math.min(...xs);
const bottom = Math.max(...ys);
@@ -461,30 +609,117 @@ export default class GestureOverlay extends Touchable {
return { right: right, left: left, bottom: bottom, top: top, width: right - left, height: bottom - top };
}
- @computed get currentStroke() {
- if (this._points.length <= 1) {
- return (null);
- }
+ @computed get svgBounds() {
+ return this.getBounds(this._points);
+ }
+ @computed get elements() {
const B = this.svgBounds;
+ return [
+ this.props.children,
+ this._palette,
+ [this._strokes.map(l => {
+ const b = this.getBounds(l);
+ return <svg key={b.left} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
+ {InteractionUtils.CreatePolyline(l, b.left, b.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)}
+ </svg>;
+ }),
+ this._points.length <= 1 ? (null) : <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
+ {InteractionUtils.CreatePolyline(this._points, B.left, B.top, GestureOverlay.Instance.Color, GestureOverlay.Instance.Width)}
+ </svg>]
+ ];
+ }
- return (
- <svg width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000 }}>
- {CreatePolyline(this._points, B.left, B.top, this.Color, this.Width)}
- </svg>
- );
+ @action
+ public openFloatingDoc = (doc: Doc) => {
+ this._clipboardDoc =
+ <DocumentView
+ Document={doc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ onClick={undefined}
+ removeDocument={undefined}
+ ScreenToLocalTransform={() => new Transform(-(this._thumbX ?? 0), -(this._thumbY ?? 0) + this.height, 1)}
+ ContentScaling={returnOne}
+ PanelWidth={() => 300}
+ PanelHeight={() => 300}
+ renderDepth={0}
+ backgroundColor={returnEmptyString}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />;
+ }
+
+ @action
+ public closeFloatingDoc = () => {
+ this._clipboardDoc = undefined;
}
render() {
+ trace();
return (
<div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
- {this.props.children}
- {this._palette}
- {this.currentStroke}
+ {this.elements}
+
+ <div className="clipboardDoc-cont" style={{
+ transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
+ height: this.height * 2,
+ width: this.height * 2,
+ pointerEvents: this._clipboardDoc ? "unset" : "none",
+ touchAction: this._clipboardDoc ? "unset" : "none",
+ }}>
+ {this._clipboardDoc}
+ </div>
+ <div className="filter-cont" style={{
+ transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
+ height: this.height * 2,
+ width: this.height * 2,
+ pointerEvents: "none",
+ touchAction: "none",
+ display: this.showBounds ? "unset" : "none",
+ }}>
+ </div>
+ <TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} />
</div>);
}
}
+// export class
+
+export enum ToolglassTools {
+ InkToText = "inktotext",
+ IgnoreGesture = "ignoregesture",
+ RadialMenu = "radialmenu",
+ None = "none",
+}
+
Scripting.addGlobal("GestureOverlay", GestureOverlay);
-Scripting.addGlobal(function setPen(width: any, color: any) { runInAction(() => { GestureOverlay.Instance.Color = color; GestureOverlay.Instance.Width = width; }); });
-Scripting.addGlobal(function resetPen() { runInAction(() => { GestureOverlay.Instance.Color = "rgb(244, 67, 54)"; GestureOverlay.Instance.Width = 5; }); }); \ No newline at end of file
+Scripting.addGlobal(function setToolglass(tool: any) {
+ runInAction(() => GestureOverlay.Instance.Tool = tool);
+});
+Scripting.addGlobal(function setPen(width: any, color: any) {
+ runInAction(() => {
+ GestureOverlay.Instance.SavedColor = GestureOverlay.Instance.Color;
+ GestureOverlay.Instance.Color = color;
+ GestureOverlay.Instance.SavedWidth = GestureOverlay.Instance.Width;
+ GestureOverlay.Instance.Width = width;
+ });
+});
+Scripting.addGlobal(function resetPen() {
+ runInAction(() => {
+ GestureOverlay.Instance.Color = GestureOverlay.Instance.SavedColor ?? "rgb(244, 67, 54)";
+ GestureOverlay.Instance.Width = GestureOverlay.Instance.SavedWidth ?? 5;
+ });
+});
+Scripting.addGlobal(function createText(text: any, x: any, y: any) {
+ GestureOverlay.Instance.dispatchGesture("text", [{ X: x, Y: y }], text);
+}); \ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index be07a9024..6cee702ee 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -2,14 +2,11 @@ import { action, computed, observable } from "mobx";
import { ColorState } from 'react-color';
import { Doc } from "../../new_fields/Doc";
import { InkTool } from "../../new_fields/InkField";
-import { List } from "../../new_fields/List";
-import { listSpec } from "../../new_fields/Schema";
-import { Cast, NumCast, StrCast, FieldValue } from "../../new_fields/Types";
-import { Utils } from "../../Utils";
+import { FieldValue, NumCast, StrCast } from "../../new_fields/Types";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
-import { undoBatch, UndoManager } from "../util/UndoManager";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { undoBatch } from "../util/UndoManager";
import GestureOverlay from "./GestureOverlay";
export class InkingControl {
@@ -41,59 +38,12 @@ export class InkingControl {
if (InkingControl.Instance.selectedTool === InkTool.None) {
const selected = SelectionManager.SelectedDocuments();
- const oldColors = selected.map(view => {
+ selected.map(view => {
const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
view.props.Document.layout instanceof Doc ? view.props.Document.layout :
- view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document);
- const sel = window.getSelection();
- if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
- targetDoc.color = this._selectedColor;
- return {
- target: targetDoc,
- previous: StrCast(targetDoc.color)
- };
- }
- const oldColor = StrCast(targetDoc.backgroundColor);
- let matchedColor = this._selectedColor;
- const cvd = view.props.ContainingCollectionDoc;
- let ruleProvider = view.props.ruleProvider;
- if (cvd) {
- if (!cvd.colorPalette) {
- const defaultPalette = ["rg(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
- "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
- const colorPalette = Cast(cvd.colorPalette, listSpec("string"));
- if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette);
- }
- const cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
- let closest = 0;
- let dist = 10000000;
- const ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
- for (let i = 0; i < cp.length; i++) {
- const cpcol = Utils.fromRGBAstr(cp[i]);
- const d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g));
- if (d < dist) {
- dist = d;
- closest = i;
- }
- }
- cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")";
- cvd.colorPalette = new List(cp);
- matchedColor = cp[closest];
- ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
- ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
- }
- (!ruleProvider && targetDoc) && (Doc.Layout(view.props.Document).backgroundColor = matchedColor);
-
- return {
- target: targetDoc,
- previous: oldColor
- };
+ view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
+ targetDoc && (Doc.Layout(view.props.Document).backgroundColor = CurrentUserUtils.UserDocument.inkColor);
});
- //let captured = this._selectedColor;
- // UndoManager.AddEvent({
- // undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
- // redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
- // });
} else {
CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8b346d5d9..f315ce12a 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -10,24 +10,17 @@ import "./InkingStroke.scss";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
import React = require("react");
import { TraceMobx } from "../../new_fields/util";
+import { InteractionUtils } from "../util/InteractionUtils";
+import { ContextMenu } from "./ContextMenu";
+import { CognitiveServices } from "../cognitive_services/CognitiveServices";
+import { faPaintBrush } from "@fortawesome/free-solid-svg-icons";
+import { library } from "@fortawesome/fontawesome-svg-core";
+
+library.add(faPaintBrush);
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
-export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color?: string, width?: number) {
- const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
- return (
- <polyline
- points={pts}
- style={{
- fill: "none",
- stroke: color ?? InkingControl.Instance.selectedColor,
- strokeWidth: width ?? InkingControl.Instance.selectedWidth
- }}
- />
- );
-}
-
@observer
export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocument>(InkDocument) {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
@@ -35,6 +28,11 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
@computed get PanelWidth() { return this.props.PanelWidth(); }
@computed get PanelHeight() { return this.props.PanelHeight(); }
+ private analyzeStrokes = () => {
+ const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+ CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]);
+ }
+
render() {
TraceMobx();
const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
@@ -44,18 +42,29 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
const top = Math.min(...ys);
const right = Math.max(...xs);
const bottom = Math.max(...ys);
- const points = CreatePolyline(data, left, top, this.Document.color, this.Document.strokeWidth);
+ const points = InteractionUtils.CreatePolyline(data, left, top, this.Document.color ?? InkingControl.Instance.selectedColor, this.Document.strokeWidth ?? parseInt(InkingControl.Instance.selectedWidth));
const width = right - left;
const height = bottom - top;
const scaleX = this.PanelWidth / width;
const scaleY = this.PanelHeight / height;
return (
- <svg width={width} height={height} style={{
- transformOrigin: "top left",
- transform: `scale(${scaleX}, ${scaleY})`,
- mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset",
- pointerEvents: "all"
- }}>
+ <svg
+ width={width}
+ height={height}
+ style={{
+ transformOrigin: "top left",
+ transform: `scale(${scaleX}, ${scaleY})`,
+ mixBlendMode: this.Document.tool === InkTool.Highlighter ? "multiply" : "unset",
+ pointerEvents: "all"
+ }}
+ onContextMenu={() => {
+ ContextMenu.Instance.addItem({
+ description: "Analyze Stroke",
+ event: this.analyzeStrokes,
+ icon: "paint-brush"
+ });
+ }}
+ >
{points}
</svg>
);
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 2a1cc3303..d39c217ec 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -58,13 +58,29 @@
overflow: hidden;
}
+
+.mainView-settings {
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ font-size: 8px;
+}
+
+.mainView-settings:hover {
+ transform: none !important;
+}
+
.mainView-logout {
position: absolute;
- right: 5;
- bottom: 5;
+ right: 0;
+ bottom: 0;
font-size: 8px;
}
+.mainView-logout:hover {
+ transform: none !important;
+}
+
.mainView-libraryFlyout {
height: 100%;
width: 100%;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 93fb0a07c..87a81504c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import {
- faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
- faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp
+ faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
+ faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -22,7 +22,7 @@ import { Docs, DocumentOptions } from '../documents/Documents';
import { HistoryUtil } from '../util/History';
import SharingManager from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionLinearView } from './collections/CollectionLinearView';
import { CollectionViewType, CollectionView } from './collections/CollectionView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
@@ -39,13 +39,15 @@ import MarqueeOptionsMenu from './collections/collectionFreeForm/MarqueeOptionsM
import GestureOverlay from './GestureOverlay';
import { Scripting } from '../util/Scripting';
import { AudioBox } from './nodes/AudioBox';
+import SettingsManager from '../util/SettingsManager';
import { TraceMobx } from '../../new_fields/util';
import { RadialMenu } from './nodes/RadialMenu';
+import RichTextMenu from '../util/RichTextMenu';
@observer
export class MainView extends React.Component {
public static Instance: MainView;
- private _buttonBarHeight = 75;
+ private _buttonBarHeight = 35;
private _flyoutSizeOnDown = 0;
private _urlState: HistoryUtil.DocUrl;
private _docBtnRef = React.createRef<HTMLDivElement>();
@@ -62,7 +64,7 @@ export class MainView extends React.Component {
public isPointerDown = false;
- componentWillMount() {
+ componentDidMount() {
const tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
@@ -129,6 +131,8 @@ export class MainView extends React.Component {
library.add(faLongArrowAltRight);
library.add(faCheck);
library.add(faCaretUp);
+ library.add(faFilter);
+ library.add(faBullseye);
library.add(faArrowDown);
library.add(faArrowUp);
library.add(faCloudUploadAlt);
@@ -137,6 +141,7 @@ export class MainView extends React.Component {
library.add(faEllipsisV);
library.add(faMusic);
library.add(faPhone);
+ library.add(faClipboard);
library.add(faStamp);
this.initEventListeners();
this.initAuthenticationRouters();
@@ -173,9 +178,9 @@ export class MainView extends React.Component {
} else {
if (received && this._urlState.sharing) {
reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
- initialized => initialized && received && DocServer.GetRefField(received).then(field => {
- if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
- CollectionDockingView.AddRightSplit(field, undefined);
+ initialized => initialized && received && DocServer.GetRefField(received).then(docField => {
+ if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) {
+ CollectionDockingView.AddRightSplit(docField, undefined);
}
}),
);
@@ -196,8 +201,8 @@ export class MainView extends React.Component {
const freeformOptions: DocumentOptions = {
x: 0,
y: 400,
- width: this._panelWidth * .7,
- height: this._panelHeight,
+ _width: this._panelWidth * .7,
+ _height: this._panelHeight,
title: "Collection " + workspaceCount,
backgroundColor: "white"
};
@@ -273,7 +278,6 @@ export class MainView extends React.Component {
addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
onClick={undefined}
- ruleProvider={undefined}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
@@ -304,8 +308,10 @@ export class MainView extends React.Component {
</Measure>;
}
+ _canClick = false;
onPointerDown = (e: React.PointerEvent) => {
if (this._flyoutTranslate) {
+ this._canClick = true;
this._flyoutSizeOnDown = e.clientX;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -336,11 +342,12 @@ export class MainView extends React.Component {
@action
onPointerMove = (e: PointerEvent) => {
this.flyoutWidth = Math.max(e.clientX, 0);
+ Math.abs(this.flyoutWidth - this._flyoutSizeOnDown) > 6 && (this._canClick = false);
this.sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30;
}
@action
onPointerUp = (e: PointerEvent) => {
- if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4) {
+ if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4 && this._canClick) {
this.flyoutWidth = this.flyoutWidth < 15 ? 250 : 0;
this.flyoutWidth && (this.sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30);
}
@@ -351,7 +358,7 @@ export class MainView extends React.Component {
addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string, libraryPath?: Doc[]): boolean => {
return where === "close" ? CollectionDockingView.CloseRightSplit(doc) :
doc.dockingConfig ? this.openWorkspace(doc) :
- CollectionDockingView.AddRightSplit(doc, undefined, undefined, libraryPath);
+ CollectionDockingView.AddRightSplit(doc, undefined, libraryPath);
}
mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
@@ -371,7 +378,6 @@ export class MainView extends React.Component {
addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
removeDocument={undefined}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
@@ -398,7 +404,6 @@ export class MainView extends React.Component {
addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
removeDocument={returnFalse}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={this.mainContainerXf}
ContentScaling={returnOne}
@@ -415,6 +420,9 @@ export class MainView extends React.Component {
zoomToScale={emptyFunction}
getScale={returnOne}>
</DocumentView>
+ <button className="mainView-settings" key="settings" onClick={() => SettingsManager.Instance.open()}>
+ Settings
+ </button>
<button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
</button>
@@ -491,7 +499,6 @@ export class MainView extends React.Component {
addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
removeDocument={this.remButtonDoc}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={this.buttonBarXf}
ContentScaling={returnOne}
@@ -511,6 +518,7 @@ export class MainView extends React.Component {
return (<div id="mainView-container">
<DictationOverlay />
<SharingManager />
+ <SettingsManager />
<GoogleAuthenticationManager />
<DocumentDecorations />
<GestureOverlay>
@@ -518,9 +526,10 @@ export class MainView extends React.Component {
</GestureOverlay>
<PreviewCursor />
<ContextMenu />
- <RadialMenu/>
+ <RadialMenu />
<PDFMenu />
<MarqueeOptionsMenu />
+ <RichTextMenu />
<OverlayView />
</div >);
}
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 7da55fd1c..5f4a52c0c 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -8,6 +8,10 @@
}
}
+#metadataEntry-outer {
+ overflow: auto !important;
+}
+
.metadataEntry-keys {
max-height: 80;
overflow-y: auto;
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 243cdb8f6..23b21ae0c 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -19,7 +19,6 @@ export interface MetadataEntryProps {
export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
@observable private _currentKey: string = "";
@observable private _currentValue: string = "";
- @observable private suggestions: string[] = [];
private _addChildren: boolean = false;
@observable _allSuggestions: string[] = [];
_suggestionDispser: IReactionDisposer | undefined;
@@ -178,11 +177,11 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
}
docSource = docSource as Doc[] | Doc;
if (docSource instanceof Doc) {
- if (docSource.viewType === undefined) {
+ if (docSource._viewType === undefined) {
return (null);
}
} else if (Array.isArray(docSource)) {
- if (!docSource.every(doc => doc.viewType !== undefined)) {
+ if (!docSource.every(doc => doc._viewType !== undefined)) {
return null;
}
}
@@ -197,7 +196,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
_ref = React.createRef<HTMLInputElement>();
render() {
return (
- <div className="metadataEntry-outerDiv">
+ <div className="metadataEntry-outerDiv" id="metadataEntry-outer">
<div className="metadataEntry-inputArea">
Key:
<Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
diff --git a/src/client/views/OCRUtils.ts b/src/client/views/OCRUtils.ts
new file mode 100644
index 000000000..282ec770e
--- /dev/null
+++ b/src/client/views/OCRUtils.ts
@@ -0,0 +1,7 @@
+// import tesseract from "node-tesseract-ocr";
+// const tesseract = require("node-tesseract");
+
+
+export namespace OCRUtils {
+
+}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 350a75d29..295cd7c6e 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -178,7 +178,6 @@ export class OverlayView extends React.Component {
// select={emptyFunction}
// layoutKey={"layout"}
bringToFront={emptyFunction}
- ruleProvider={undefined}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={returnOne}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 811c24f53..10aac96a0 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -56,7 +56,6 @@ export default class Palette extends React.Component<PaletteProps> {
addDocTab={returnFalse}
pinToPres={emptyFunction}
removeDocument={undefined}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 9706d0f99..c011adb20 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -31,8 +31,8 @@ export class PreviewCursor extends React.Component<{}> {
if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) {
const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/");
return PreviewCursor._addDocument(Docs.Create.VideoDocument(url, {
- title: url, width: 400, height: 315,
- nativeWidth: 600, nativeHeight: 472.5,
+ title: url, _width: 400, _height: 315,
+ _nativeWidth: 600, _nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
}));
}
@@ -42,17 +42,17 @@ export class PreviewCursor extends React.Component<{}> {
if (re.test(e.clipboardData.getData("text/plain"))) {
const url = e.clipboardData.getData("text/plain");
return PreviewCursor._addDocument(Docs.Create.WebDocument(url, {
- title: url, width: 500, height: 300,
+ title: url, _width: 500, _height: 300,
// nativeWidth: 300, nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
}));
}
// creates text document
- return PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument({
- width: 500,
+ return PreviewCursor._addLiveTextDoc(Docs.Create.TextDocument("", {
+ _width: 500,
limitHeight: 400,
- autoHeight: true,
+ _autoHeight: true,
x: newPoint[0],
y: newPoint[1],
title: "-pasted text-"
@@ -65,7 +65,7 @@ export class PreviewCursor extends React.Component<{}> {
return PreviewCursor._addDocument(Docs.Create.ImageDocument(
arr[1], {
- width: 300, title: arr[1],
+ _width: 300, title: arr[1],
x: newPoint[0],
y: newPoint[1],
}));
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index ded2329b4..d24256886 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -82,28 +82,19 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
);
}
//let l = docList(this.source[0].data).length; if (l) { let ind = this.target[0].index !== undefined ? (this.target[0].index+1) % l : 0; this.target[0].index = ind; this.target[0].proto = getProto(docList(this.source[0].data)[ind]);}
- public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, prewrapper?: string, postwrapper?: string) {
+ public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }) {
let overlayDisposer: () => void = emptyFunction;
const script = ScriptCast(doc[fieldKey]);
let originalText: string | undefined = undefined;
if (script) {
originalText = script.script.originalScript;
- if (prewrapper && originalText.startsWith(prewrapper)) {
- originalText = originalText.substr(prewrapper.length);
- }
- if (postwrapper && originalText.endsWith(postwrapper)) {
- originalText = originalText.substr(0, originalText.length - postwrapper.length);
- }
}
// tslint:disable-next-line: no-unnecessary-callback-wrapper
const params: string[] = [];
const setParams = (p: string[]) => params.splice(0, params.length, ...p);
const scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
- if (prewrapper) {
- text = prewrapper + text + (postwrapper ? postwrapper : "");
- }
const script = CompileScript(text, {
- params: { this: Doc.name },
+ params: { this: Doc.name, ...contextParams },
typecheck: false,
editable: true,
transformer: DocumentIconContainer.getTransformer()
diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss
index 69bebe0e9..bbed8cd96 100644
--- a/src/client/views/TemplateMenu.scss
+++ b/src/client/views/TemplateMenu.scss
@@ -15,12 +15,12 @@
.templating-button {
width: 20px;
height: 20px;
- border-radius: 50%;
- opacity: 0.9;
- font-size: 14;
- background-color: $dark-color;
- color: $light-color;
- text-align: center;
+ padding-left: 5px;
+ background: black;
+ color: white;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
cursor: pointer;
&:hover {
@@ -42,6 +42,7 @@
.templateToggle, .chromeToggle {
text-align: left;
+ color: black;
}
input {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 10419ddb7..f61eb9cd0 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,14 +1,14 @@
-import { action, observable } from "mobx";
+import { action, observable, runInAction, ObservableSet } from "mobx";
import { observer } from "mobx-react";
-import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import './TemplateMenu.scss';
import { DocumentView } from "./nodes/DocumentView";
import { Template, Templates } from "./Templates";
import React = require("react");
-import { Doc } from "../../new_fields/Doc";
-import { StrCast } from "../../new_fields/Types";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { StrCast, Cast } from "../../new_fields/Types";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -41,7 +41,7 @@ class OtherToggle extends React.Component<{ checked: boolean, name: string, togg
}
export interface TemplateMenuProps {
- docs: DocumentView[];
+ docViews: DocumentView[];
templates: Map<Template, boolean>;
}
@@ -49,17 +49,14 @@ export interface TemplateMenuProps {
@observer
export class TemplateMenu extends React.Component<TemplateMenuProps> {
@observable private _hidden: boolean = true;
- private _downx = 0;
- private _downy = 0;
- private _dragRef = React.createRef<HTMLUListElement>();
- toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => {
- this.props.docs.map(dv => dv.setCustomView(e.target.checked));
+ toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => {
+ this.props.docViews.map(dv => dv.setCustomView(e.target.checked, layout));
}
toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
SelectionManager.DeselectAll();
- const topDocView = this.props.docs[0];
+ const topDocView = this.props.docViews[0];
const ex = e.target.getBoundingClientRect().left;
const ey = e.target.getBoundingClientRect().top;
DocumentView.FloatDoc(topDocView, ex, ey);
@@ -70,25 +67,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
if (event.target.checked) {
- this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase());
+ this.props.docViews.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase());
} else {
- this.props.docs.map(d => d.Document["show" + template.Name] = "");
+ this.props.docViews.map(d => d.Document["show" + template.Name] = "");
}
}
- @undoBatch
- @action
- clearTemplates = (event: React.MouseEvent) => {
- Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined));
- ["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => {
- if (d.Document.isTemplateDoc && d.props.DataDoc) {
- d.Document[field] = undefined;
- } else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) {
- d.Document[field] = Doc.GetProto(d.Document)[field] = undefined;
- }
- }));
- }
-
@action
toggleTemplateActivity = (): void => {
this._hidden = !this._hidden;
@@ -97,64 +81,45 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@undoBatch
@action
toggleChrome = (): void => {
- this.props.docs.map(dv => {
+ this.props.docViews.map(dv => {
const layout = Doc.Layout(dv.Document);
- layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
+ layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : "enabled");
});
}
- onAliasButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onAliasButtonMoved);
- document.removeEventListener("pointerup", this.onAliasButtonUp);
- e.stopPropagation();
- }
- onAliasButtonDown = (e: React.PointerEvent): void => {
- this._downx = e.clientX;
- this._downy = e.clientY;
- e.stopPropagation();
- e.preventDefault();
- document.removeEventListener("pointermove", this.onAliasButtonMoved);
- document.addEventListener("pointermove", this.onAliasButtonMoved);
- document.removeEventListener("pointerup", this.onAliasButtonUp);
- document.addEventListener("pointerup", this.onAliasButtonUp);
- }
- onAliasButtonMoved = (e: PointerEvent): void => {
- if (this._dragRef.current !== null && (Math.abs(e.clientX - this._downx) > 4 || Math.abs(e.clientY - this._downy) > 4)) {
- document.removeEventListener("pointermove", this.onAliasButtonMoved);
- document.removeEventListener("pointerup", this.onAliasButtonUp);
-
- const dragDocView = this.props.docs[0];
- const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
- const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- dragData.embedDoc = true;
- dragData.dropAction = "alias";
- DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, {
- offsetX: dragData.offset[0],
- offsetY: dragData.offset[1],
- hideSource: false
- });
+ // todo: add brushes to brushMap to save with a style name
+ onCustomKeypress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ runInAction(() => this._addedKeys.add(this._customRef.current!.value));
}
- e.stopPropagation();
+ }
+ componentDidMount() {
+ !this._addedKeys && (this._addedKeys = new ObservableSet());
+ Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))).
+ filter(key => key.startsWith("layout_")).
+ map(key => runInAction(() => this._addedKeys.add(key.replace("layout_", ""))));
+ DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => {
+ if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title)) {
+ runInAction(() => this._addedKeys.add(StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title)));
+ }
+ });
}
+ _addedKeys = new ObservableSet();
+ _customRef = React.createRef<HTMLInputElement>();
render() {
- const layout = Doc.Layout(this.props.docs[0].Document);
+ const layout = Doc.Layout(this.props.docViews[0].Document);
const templateMenu: Array<JSX.Element> = [];
this.props.templates.forEach((checked, template) =>
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
- templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
- templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
- templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
- return (
- <Flyout anchorPoint={anchorPoints.RIGHT_TOP}
- content={<ul className="template-list" ref={this._dragRef} style={{ display: this._hidden ? "none" : "block" }}>
- {templateMenu}
- {<button onClick={this.clearTemplates}>Restore Defaults</button>}
- </ul>}>
- <div className="templating-menu" onPointerDown={this.onAliasButtonDown}>
- <div title="Drag:(create alias). Tap:(modify layout)." className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
- </div>
- </Flyout>
+ templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docViews[0].Document.z ? true : false} toggle={this.toggleFloat} />);
+ templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
+ this._addedKeys && Array.from(this._addedKeys).map(layout =>
+ templateMenu.push(<OtherToggle key={layout} name={layout} checked={StrCast(this.props.docViews[0].Document.layoutKey, "layout") === "layout_" + layout} toggle={e => this.toggleLayout(e, layout)} />)
);
+ return <ul className="template-list" style={{ display: "block" }}>
+ {templateMenu}
+ <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress}></input>
+ </ul>;
}
} \ No newline at end of file
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
index ef78b60d4..8c60f1c36 100644
--- a/src/client/views/Templates.tsx
+++ b/src/client/views/Templates.tsx
@@ -43,7 +43,7 @@ export namespace Templates {
`<div>
<div style="height:100%; width:100%;">{layout}</div>
<div style="bottom: 0; font-size:14px; width:100%; position:absolute">
- <FormattedTextBox {...props} height="min-content" fieldKey={"caption"} hideOnLeave={"true"} />
+ <FormattedTextBox {...props} fieldKey={"caption"} hideOnLeave={"true"} />
</div>
</div>` );
@@ -56,8 +56,17 @@ export namespace Templates {
<div style="width:100%;overflow:auto">{layout}</div>
</div>
</div>` );
+ export const TitleHover = new Template("TitleHover", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:25px; width:100%; background-color: rgba(0, 0, 0, .4); color: white; z-index: 100">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div>
+ <div style="height:calc(100% - 25px);">
+ <div style="width:100%;overflow:auto">{layout}</div>
+ </div>
+ </div>` );
- export const TemplateList: Template[] = [Title, Caption];
+ export const TemplateList: Template[] = [Title, TitleHover, Caption];
export function sortTemplates(a: Template, b: Template) {
if (a.Position < b.Position) { return -1; }
diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx
new file mode 100644
index 000000000..4bda0818e
--- /dev/null
+++ b/src/client/views/TouchScrollableMenu.tsx
@@ -0,0 +1,59 @@
+import React = require("react");
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+
+export interface TouchScrollableMenuProps {
+ options: JSX.Element[];
+ bounds: {
+ right: number;
+ left: number;
+ bottom: number;
+ top: number;
+ width: number;
+ height: number;
+ };
+ selectedIndex: number;
+ x: number;
+ y: number;
+}
+
+export interface TouchScrollableMenuItemProps {
+ text: string;
+ onClick: () => any;
+}
+
+@observer
+export default class TouchScrollableMenu extends React.Component<TouchScrollableMenuProps> {
+
+ @computed
+ private get possibilities() { return this.props.options; }
+
+ @computed
+ private get selectedIndex() { return this.props.selectedIndex; }
+
+ render() {
+ return (
+ <div className="inkToTextDoc-cont" style={{
+ transform: `translate(${this.props.x}px, ${this.props.y}px)`,
+ width: 300,
+ height: this.possibilities.length * 25
+ }}>
+ <div className="inkToTextDoc-scroller" style={{ transform: `translate(0, ${-this.selectedIndex * 25}px)` }}>
+ {this.possibilities}
+ </div>
+ <div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}>
+ </div>
+ </div>
+ )
+ }
+}
+
+export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenuItemProps>{
+ render() {
+ return (
+ <div className="menuItem-cont" onClick={this.props.onClick}>
+ {this.props.text}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
new file mode 100644
index 000000000..4815f1a59
--- /dev/null
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -0,0 +1,40 @@
+
+.collectionCarouselView-outer {
+ background: gray;
+ .collectionCarouselView-caption {
+ margin-left: 10%;
+ margin-right: 10%;
+ height: 50;
+ display: inline-block;
+ width: 80%;
+ }
+ .collectionCarouselView-image {
+ height: calc(100% - 50px);
+ display: inline-block;
+ width: 100%;
+ }
+}
+.carouselView-back {
+ position: absolute;
+ display: flex;
+ left: 0;
+ top: 50%;
+ width: 30;
+ height: 30;
+ background: lightgray;
+ align-items: center;
+ border-radius: 5px;
+ justify-content: center;
+}
+.carouselView-fwd {
+ position: absolute;
+ display: flex;
+ right: 0;
+ top: 50%;
+ width: 30;
+ height: 30;
+ background: lightgray;
+ align-items: center;
+ border-radius: 5px;
+ justify-content: center;
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
new file mode 100644
index 000000000..0933d5924
--- /dev/null
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -0,0 +1,82 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observable, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { makeInterface } from '../../../new_fields/Schema';
+import { NumCast, StrCast } from '../../../new_fields/Types';
+import { DragManager } from '../../util/DragManager';
+import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
+import "./CollectionCarouselView.scss";
+import { CollectionSubView } from './CollectionSubView';
+import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
+import { Doc } from '../../../new_fields/Doc';
+import { FormattedTextBox } from '../nodes/FormattedTextBox';
+
+
+
+
+type CarouselDocument = makeInterface<[typeof documentSchema,]>;
+const CarouselDocument = makeInterface(documentSchema);
+
+@observer
+export class CollectionCarouselView extends CollectionSubView(CarouselDocument) {
+ @observable public addMenuToggle = React.createRef<HTMLInputElement>();
+ private _dropDisposer?: DragManager.DragDropDisposer;
+
+ componentWillUnmount() {
+ this._dropDisposer && this._dropDisposer();
+ }
+
+ componentDidMount() {
+ }
+ protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ }
+ }
+
+ advance = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length;
+ }
+ goback = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+ }
+
+ panelHeight = () => this.props.PanelHeight() - 50;
+ @computed get content() {
+ const index = NumCast(this.layoutDoc._itemIndex);
+ return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) :
+ <div>
+ <div className="collectionCarouselView-image">
+ <ContentFittingDocumentView {...this.props}
+ Document={this.childLayoutPairs[index].layout}
+ DataDocument={this.childLayoutPairs[index].data}
+ PanelHeight={this.panelHeight}
+ getTransform={this.props.ScreenToLocalTransform} />
+ </div>
+ <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}>
+ <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox>
+ </div>
+ </div>
+ }
+ @computed get buttons() {
+ return <>
+ <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}>
+ <FontAwesomeIcon icon={faCaretLeft} size={"2x"} />
+ </div>
+ <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}>
+ <FontAwesomeIcon icon={faCaretRight} size={"2x"} />
+ </div>
+ </>;
+ }
+ render() {
+ return <div className="collectionCarouselView-outer">
+ {this.content}
+ {this.buttons}
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 151b84c50..82cb3bc88 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -34,6 +34,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { ComputedField } from '../../../new_fields/ScriptField';
import { InteractionUtils } from '../../util/InteractionUtils';
import { TraceMobx } from '../../../new_fields/util';
+import { Scripting } from '../../util/Scripting';
library.add(faFile);
const _global = (window /* browser */ || global /* node */) as any;
@@ -131,7 +132,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
@action
- public static CloseRightSplit(document: Doc): boolean {
+ public static CloseRightSplit(document: Opt<Doc>): boolean {
if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
let retVal = false;
@@ -139,14 +140,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) &&
- Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
+ ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) ||
+ (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) {
child.contentItems[0].remove();
instance.layoutChanged(document);
return true;
} else {
Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) &&
- Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+ ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) ||
+ (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)))) {
child.contentItems[j].remove();
child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
return true;
@@ -171,13 +174,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
this.stateChanged();
}
+ @undoBatch
+ @action
+ public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean {
+ if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance;
+ const newItemStackConfig = {
+ type: 'stack',
+ content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
+ };
+
+ const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
+
+ let retVal = false;
+ if (instance._goldenLayout.root.contentItems[0].isRow) {
+ retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanle) {
+ child.contentItems[0].remove();
+ child.addChild(newContentItem, undefined, true);
+ instance.layoutChanged(document);
+ return true;
+ } else {
+ Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
+ child.contentItems[j].remove();
+ child.addChild(newContentItem, undefined, true);
+ return true;
+ }
+ return false;
+ });
+ }
+ return false;
+ });
+ }
+ if (retVal) {
+ instance.stateChanged();
+ }
+ return retVal;
+ }
+
//
- // Creates a vertical split on the right side of the docking view, and then adds the Document to that split
+ // Creates a vertical split on the right side of the docking view, and then adds the Document to the right of that split
//
@undoBatch
@action
- public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false, libraryPath?: Doc[]) {
+ public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
const newItemStackConfig = {
@@ -202,16 +244,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
collayout.config.width = 50;
newContentItem.config.width = 50;
}
- if (minimize) {
- // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
- // newContentItem.config.width = 10;
- // newContentItem.config.height = 10;
- }
newContentItem.callDownwards('_$init');
instance.layoutChanged();
return true;
}
+ //
+ // Creates a vertical split on the right side of the docking view, and then adds the Document to that split
+ //
+ @undoBatch
+ @action
+ public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ document.isDisplayPanel = true;
+ if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) {
+ CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath);
+ }
+ }
+
@undoBatch
@action
public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => {
@@ -476,7 +525,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
stack.header.element.on('mousedown', (e: any) => {
if (e.target === stack.header.element[0] && e.button === 1) {
- this.AddTab(stack, Docs.Create.FreeformDocument([], { width: this.props.PanelWidth(), height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+ this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
}
});
@@ -596,7 +645,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (curPres) {
const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
Doc.GetProto(pinDoc).presentationTargetDoc = doc;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
+ Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title?.toString()');
const data = Cast(curPres.data, listSpec(Doc));
if (data) {
data.push(pinDoc);
@@ -636,19 +685,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
get layoutDoc() { return this._document && Doc.Layout(this._document); }
- panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.nativeWidth)), this._panelWidth) : this._panelWidth;
+ panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth;
panelHeight = () => this._panelHeight;
- nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0;
+ nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
+ nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
contentScaling = () => {
if (this.layoutDoc!.type === DocumentType.PDF) {
- if ((this.layoutDoc && this.layoutDoc.fitWidth) ||
- this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) {
- return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth);
+ if ((this.layoutDoc && this.layoutDoc._fitWidth) ||
+ this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) {
+ return this._panelWidth / NumCast(this.layoutDoc!._nativeWidth);
} else {
- return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight);
+ return this._panelHeight / NumCast(this.layoutDoc!._nativeHeight);
}
}
const nativeH = this.nativeHeight();
@@ -674,7 +723,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (doc.dockingConfig) {
return MainView.Instance.openWorkspace(doc);
} else if (location === "onRight") {
- return CollectionDockingView.AddRightSplit(doc, dataDoc, undefined, libraryPath);
+ return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath);
} else if (location === "close") {
return CollectionDockingView.CloseRightSplit(doc);
} else {
@@ -694,7 +743,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
- ruleProvider={undefined}
ContentScaling={this.contentScaling}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
@@ -717,10 +765,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
(<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
style={{
transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
- height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%",
+ height: this.layoutDoc && this.layoutDoc._fitWidth ? undefined : "100%",
width: this.widthpercent
}}>
{this.docView}
</div >);
}
}
+Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
+Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); });
diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss
index 81210d7ae..eae9e0220 100644
--- a/src/client/views/CollectionLinearView.scss
+++ b/src/client/views/collections/CollectionLinearView.scss
@@ -1,5 +1,5 @@
-@import "globalCssVariables";
-@import "nodeModuleOverrides";
+@import "../globalCssVariables";
+@import "../_nodeModuleOverrides";
.collectionLinearView-outer{
overflow: hidden;
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index 0a3096833..e613bf411 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -1,19 +1,19 @@
import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
-import { makeInterface } from '../../new_fields/Schema';
-import { BoolCast, NumCast, StrCast, Cast } from '../../new_fields/Types';
-import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
-import { DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
+import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { makeInterface } from '../../../new_fields/Schema';
+import { BoolCast, NumCast, StrCast, Cast } from '../../../new_fields/Types';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../../Utils';
+import { DragManager } from '../../util/DragManager';
+import { Transform } from '../../util/Transform';
import "./CollectionLinearView.scss";
-import { CollectionViewType } from './collections/CollectionView';
-import { CollectionSubView } from './collections/CollectionSubView';
-import { DocumentView } from './nodes/DocumentView';
-import { documentSchema } from '../../new_fields/documentSchemas';
-import { Id } from '../../new_fields/FieldSymbols';
-import { ScriptField } from '../../new_fields/ScriptField';
+import { CollectionViewType } from './CollectionView';
+import { CollectionSubView } from './CollectionSubView';
+import { DocumentView } from '../nodes/DocumentView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { ScriptField } from '../../../new_fields/ScriptField';
type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -31,18 +31,36 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
this._dropDisposer && this._dropDisposer();
this._widthDisposer && this._widthDisposer();
this._selectedDisposer && this._selectedDisposer();
+ this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
+ Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
+ });
}
componentDidMount() {
// is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
- this._widthDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
- () => this.props.Document.width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
+ () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
{ fireImmediately: true }
);
this._selectedDisposer = reaction(
() => NumCast(this.props.Document.selectedIndex),
- (i) => runInAction(() => this._selectedIndex = i),
+ (i) => runInAction(() => {
+ this._selectedIndex = i;
+ let selected: any = undefined;
+ this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => {
+ const isSelected = this._selectedIndex === ind;
+ if (isSelected) {
+ selected = pair;
+ }
+ else {
+ Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
+ }
+ });
+ if (selected && selected.layout) {
+ Cast(selected.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: selected.layout.proto }, console.log);
+ }
+ }),
{ fireImmediately: true }
);
}
@@ -55,7 +73,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
- dimension = () => NumCast(this.props.Document.height); // 2 * the padding
+ dimension = () => NumCast(this.props.Document._height); // 2 * the padding
getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
if (!ele.current) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
@@ -64,22 +82,19 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
render() {
const guid = Utils.GenerateGuid();
+ const flexDir: any = StrCast(this.Document.flexDirection);
return <div className="collectionLinearView-outer">
<div className="collectionLinearView" ref={this.createDashEventsTarget} >
<input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
<label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
- <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document.width, 25) }}>
+ <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}>
{this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
- const nested = pair.layout.viewType === CollectionViewType.Linear;
+ const nested = pair.layout._viewType === CollectionViewType.Linear;
const dref = React.createRef<HTMLDivElement>();
- const nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
+ const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension());
const deltaSize = nativeWidth * .15 / 2;
- const isSelected = this._selectedIndex === ind;
- if (isSelected) {
- Cast(pair.layout.proto?.onPointerDown, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
- }
return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
style={{
width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
@@ -94,7 +109,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
addDocTab={this.props.addDocTab}
pinToPres={emptyFunction}
removeDocument={this.props.removeDocument}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={this.getTransform(dref)}
ContentScaling={returnOne}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 80752303c..e25a2f5eb 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -136,7 +136,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
addDocument = (value: string, shiftDown?: boolean) => {
this._createAliasSelected = false;
const key = StrCast(this.props.parent.props.Document.sectionFilter);
- const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value });
newDoc[key] = this.getValue(this.props.heading);
return this.props.parent.props.addDocument(newDoc);
}
@@ -258,7 +258,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@computed get contentLayout() {
const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
const style = this.props.parent; const collapsed = this._collapsed;
- const chromeStatus = this.props.parent.props.Document.chromeStatus;
+ const chromeStatus = this.props.parent.props.Document._chromeStatus;
const newEditableViewProps = {
GetValue: () => "",
SetValue: this.addDocument,
diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionPivotView.scss
new file mode 100644
index 000000000..505091e98
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.scss
@@ -0,0 +1,88 @@
+.collectionPivotView {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+
+ .collectionPivotView-flyout {
+ width: 400px;
+ height: 300px;
+ display: inline-block;
+
+ .collectionPivotView-flyout-item {
+ background-color: lightgray;
+ text-align: left;
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ }
+ }
+
+ .pivotKeyEntry {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 10;
+ pointer-events: all;
+ padding: 5px;
+ border: 1px solid black;
+ }
+
+ .collectionPivotView-treeView {
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ height: 100%;
+
+ .collectionPivotView-addfacet {
+ display: inline-block;
+ width: 200px;
+ height: 30px;
+ background: darkGray;
+ text-align: center;
+
+ .collectionPivotView-button {
+ align-items: center;
+ display: flex;
+ width: 100%;
+ height: 100%;
+
+ .collectionPivotView-span {
+ margin: auto;
+ }
+ }
+
+ >div,
+ >div>div {
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ }
+ }
+
+ .collectionPivotView-tree {
+ display: inline-block;
+ width: 100%;
+ height: calc(100% - 30px);
+ }
+ }
+
+ .collectionPivotView-pivot {
+ display: inline-block;
+ width: calc(100% - 200px);
+ height: 100%;
+ }
+
+ .collectionPivotView-dragger {
+ background-color: lightgray;
+ height: 40px;
+ width: 20px;
+ position: absolute;
+ border-radius: 10px;
+ top: 55%;
+ border: 1px black solid;
+ z-index: 2;
+ left: -10px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
new file mode 100644
index 000000000..440b6856b
--- /dev/null
+++ b/src/client/views/collections/CollectionPivotView.tsx
@@ -0,0 +1,148 @@
+import { faEdit } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Set } from "typescript-collections";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { Docs } from "../../documents/Documents";
+import { EditableView } from "../EditableView";
+import { anchorPoints, Flyout } from "../TemplateMenu";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+import "./CollectionPivotView.scss";
+import { CollectionSubView } from "./CollectionSubView";
+import { CollectionTreeView } from "./CollectionTreeView";
+import React = require("react");
+
+@observer
+export class CollectionPivotView extends CollectionSubView(doc => doc) {
+ componentDidMount() {
+ this.props.Document._freeformLayoutEngine = "pivot";
+ const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
+ if (!this.props.Document._facetCollection) {
+ const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
+ facetCollection.target = this.props.Document;
+ this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]);
+
+ const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
+ const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); ";
+ facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name });
+ this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name });
+ this.props.Document._facetCollection = facetCollection;
+ this.props.Document._fitToBox = true;
+ }
+ }
+ bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0);
+
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ return facets.toArray();
+ }
+
+ /**
+ * Responds to clicking the check box in the flyout menu
+ */
+ facetClick = (facetHeader: string) => {
+ const facetCollection = this.props.Document._facetCollection;
+ if (facetCollection instanceof Doc) {
+ const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader);
+ if (found !== -1) {
+ (facetCollection.data as List<Doc>).splice(found, 1);
+ const docFilter = Cast(this.props.Document._docFilter, listSpec("string"));
+ if (docFilter) {
+ let index: number;
+ while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
+ docFilter.splice(index, 3);
+ }
+ }
+ } else {
+ const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
+ const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
+ const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
+ newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
+ Doc.AddDocToList(facetCollection, "data", newFacet);
+ }
+ }
+ }
+ _canClick = false;
+ _facetWidthOnDown = 0;
+ @observable _facetWidth = 200;
+ onPointerDown = (e: React.PointerEvent) => {
+ this._canClick = true;
+ this._facetWidthOnDown = e.screenX;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0);
+ Math.abs(e.movementX) > 6 && (this._canClick = false);
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ if (Math.abs(e.screenX - this._facetWidthOnDown) < 6 && this._canClick) {
+ this._facetWidth = this._facetWidth < 15 ? 200 : 0;
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ render() {
+ const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
+ const flyout = (
+ <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}>
+ {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
+ <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} />
+ <span className="checkmark" />
+ {facet}
+ </label>)}
+ </div>
+ );
+ return !facetCollection ? (null) :
+ <div className="collectionPivotView" style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
+ <div className={"pivotKeyEntry"}>
+ <EditableView
+ contents={this.props.Document.pivotField}
+ GetValue={() => StrCast(this.props.Document.pivotField)}
+ SetValue={value => {
+ if (value && value.length) {
+ this.props.Document.pivotField = value;
+ return true;
+ }
+ return false;
+ }}
+ />
+ </div>
+ <div className="collectionPivotView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
+ <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
+ </div>
+ <div className="collectionPivotView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
+ <div className="collectionPivotView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionPivotView-button">
+ <span className="collectionPivotView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionPivotView-tree" key="tree">
+ <CollectionTreeView {...this.props} Document={facetCollection} />
+ </div>
+ </div>
+ <div className="collectionPivotView-pivot" key="pivot" style={{ width: this.bodyPanelWidth() }}>
+ <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 79a34bc00..caffa7eb1 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -145,7 +145,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
DataDoc: this.props.rowProps.original,
LibraryPath: [],
fieldKey: this.props.rowProps.column.id as string,
- ruleProvider: undefined,
ContainingCollectionView: this.props.CollectionView,
ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
isSelected: returnFalse,
@@ -226,14 +225,14 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
if (value.startsWith(":=")) {
return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
}
- const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
if (!script.compiled) {
return false;
}
return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
}}
OnFillDown={async (value: string) => {
- const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
if (script.compiled) {
DocListCast(this.props.Document[this.props.fieldKey]).
forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run));
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index 0114342b9..92dc8780e 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -286,7 +286,6 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
}
@undoBatch
- @action
onKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === "Enter") {
const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
@@ -296,7 +295,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
this.onSelect(this._searchTerm);
} else {
- this._searchTerm = this._key;
+ this.setSearchTerm(this._key);
}
}
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index bb706e528..fa8be5177 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -144,7 +144,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
LibraryPath={this.props.LibraryPath}
childDocs={this.childDocs}
renderDepth={this.props.renderDepth}
- ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
PanelWidth={this.previewWidth}
PanelHeight={this.previewHeight}
getTransform={this.getPreviewTransform}
@@ -157,8 +156,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
- setPreviewScript={this.setPreviewScript}
- previewScript={this.previewScript}
/>
</div>;
}
@@ -480,7 +477,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
@undoBatch
createRow = () => {
- const newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 });
+ const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 });
this.props.addDocument(newDoc);
}
@@ -627,6 +624,19 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
return Array.from(Object.keys(keys));
}
+ @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;
@@ -645,7 +655,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
const expanded = {};
//@ts-ignore
expandedRowsList.forEach(row => expanded[row] = true);
- console.log("text wrapped rows", ...[...this.textWrappedRows]); // TODO: get component to rerender on text wrap change without needign to console.log :((((
+ const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :((((
return <ReactTable
style={{ position: "relative" }}
@@ -681,7 +691,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" });
+ // ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" });
+ ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" })
}
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index e1577cfee..843c743db 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -385,4 +385,4 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index b8423af20..91c7ca76e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,7 +4,7 @@ import { CursorProperty } from "csstype";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import Switch from 'rc-switch';
-import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc, HeightSym, WidthSym, DataSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
@@ -22,7 +22,6 @@ import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewField
import { CollectionSubView } from "./CollectionSubView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
-import { ScriptBox } from "../ScriptBox";
import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
import { TraceMobx } from "../../../new_fields/util";
import { CollectionViewType } from "./CollectionView";
@@ -40,35 +39,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
- @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
- @computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return Math.max(this.props.Document.showTitle ? 30 : 0, NumCast(this.props.Document.yMargin, 2 * this.gridGap)); }
- @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
+ @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); }
+ @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); }
+ @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); }
+ @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
- @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document.chromeStatus !== 'view-mode' && this.props.Document.chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
- childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
-
children(docs: Doc[]) {
this._docXfs.length = 0;
return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
- const layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
- const width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
- const height = () => this.getDocHeight(layoutDoc);
+ const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ const height = () => this.getDocHeight(d);
const dref = React.createRef<HTMLDivElement>();
- const dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ const dxf = () => this.getDocTransform(d, dref.current!);
this._docXfs.push({ dxf: dxf, width: width, height: height });
const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
+ {this.getDisplayDoc(d, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)}
</div>;
});
}
@@ -109,13 +104,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
componentDidMount() {
super.componentDidMount();
this._heightDisposer = reaction(() => {
- if (this.props.Document.autoHeight) {
+ if (this.props.Document._autoHeight) {
const sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
if (this.isStackingView) {
const res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
const r1 = Math.max(maxHght,
(this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
- const val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+ const val = height + this.getDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
return val;
}, this.yMargin));
return r1;
@@ -130,7 +125,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
},
(hgt: number) => {
const doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
- doc && hgt > 0 && (Doc.Layout(doc).height = hgt);
+ doc && hgt > 0 && (Doc.Layout(doc)._height = hgt);
},
{ fireImmediately: true }
);
@@ -156,10 +151,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
}
- overlays = (doc: Doc) => {
- return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {};
- }
-
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
@computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
@@ -170,9 +161,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
Document={doc}
DataDocument={dataDoc}
LibraryPath={this.props.LibraryPath}
- showOverlays={this.overlays}
renderDepth={this.props.renderDepth + 1}
- ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
fitToBox={this.props.fitToBox}
onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
PanelWidth={width}
@@ -187,24 +176,22 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
active={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- setPreviewScript={emptyFunction}
- previewScript={undefined}>
+ pinToPres={this.props.pinToPres}>
</ContentFittingDocumentView>;
}
getDocHeight(d?: Doc) {
if (!d) return 0;
const layoutDoc = Doc.Layout(d);
- const nw = NumCast(layoutDoc.nativeWidth);
- const nh = NumCast(layoutDoc.nativeHeight);
+ const nw = NumCast(layoutDoc._nativeWidth);
+ const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
- if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
+ if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) {
const aspect = nw && nh ? nh / nw : 1;
- if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
+ if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
return wid * aspect;
}
- return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
- Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
+ return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
+ Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
}
columnDividerDown = (e: React.PointerEvent) => {
@@ -249,7 +236,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
const pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
targInd = i;
- const axis = this.Document.viewType === CollectionViewType.Masonry ? 0 : 1;
+ const axis = this.Document._viewType === CollectionViewType.Masonry ? 0 : 1;
plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0;
}
});
@@ -294,7 +281,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const key = this.sectionFilter;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
- const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
type = types[0];
}
@@ -319,15 +306,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
const y = this._scroll; // required for document decorations to update when the text box container is scrolled
const { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
const outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ const scaling = 1 / Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]());
const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.ScreenToLocalTransform().
- translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0));
+ const offsetx = (doc[WidthSym]() - doc[WidthSym]() / scaling) / 2;
+ const offsety = (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0);
+ return this.props.ScreenToLocalTransform().translate(offset[0] - offsetx, offset[1] + offsety).scale(scaling);
}
sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
const key = this.sectionFilter;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
- const types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
type = types[0];
}
@@ -365,7 +354,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+ this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode";
}
onContextMenu = (e: React.MouseEvent): void => {
@@ -376,11 +365,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
-
- const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
- const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
- onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
- !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
}
@@ -404,20 +388,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
<div className="collectionStackingMasonry-cont" >
<div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
ref={this.createRef}
+ style={{
+ overflowY: this.props.active() ? "auto" : "hidden",
+ transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
+ height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`,
+ transformOrigin: "top"
+ }}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
onDrop={this.onDrop.bind(this)}
onContextMenu={this.onContextMenu}
- onWheel={e => e.stopPropagation()} >
+ onWheel={e => this.props.active() && 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.props.Document.chromeStatus !== 'disabled' ? <Switch
+ {this.props.Document._chromeStatus !== 'disabled' ? <Switch
onChange={this.onToggle}
onClick={this.onToggle}
- defaultChecked={this.props.Document.chromeStatus !== 'view-mode'}
+ defaultChecked={this.props.Document._chromeStatus !== 'view-mode'}
checkedChildren="edit"
unCheckedChildren="view"
/> : null}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 39b4e4e1d..2a9f903bb 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -4,7 +4,7 @@ import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
import { NumCast, StrCast } from "../../../new_fields/Types";
@@ -18,6 +18,12 @@ import { EditableView } from "../EditableView";
import { CollectionStackingView } from "./CollectionStackingView";
import "./CollectionStackingView.scss";
import { TraceMobx } from "../../../new_fields/util";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { ImageField } from "../../../new_fields/URLField";
+import { ImageBox } from "../nodes/ImageBox";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { RichTextField } from "../../../new_fields/RichTextField";
library.add(faPalette);
@@ -132,9 +138,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
addDocument = (value: string, shiftDown?: boolean) => {
+ if (!value) return false;
this._createAliasSelected = false;
const key = StrCast(this.props.parent.props.Document.sectionFilter);
- const newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
+ const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
@@ -257,6 +264,57 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@observable _headingsHack: number = 1;
+ menuCallback = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
+ const layoutItems: ContextMenuProps[] = [];
+ const docItems: ContextMenuProps[] = [];
+
+ const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document;
+ Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey =>
+ docItems.push({
+ description: ":" + fieldKey, event: () => {
+ const created = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document));
+ if (created) {
+ if (this.props.parent.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document);
+ }
+ return this.props.parent.props.addDocument(created);
+ }
+ }, icon: "compress-arrows-alt"
+ }));
+ Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => DocListCast(dataDoc[fieldKey]).length).map(fieldKey =>
+ docItems.push({
+ description: ":" + fieldKey, event: () => {
+ const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
+ if (created) {
+ if (this.props.parent.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document);
+ }
+ return this.props.parent.props.addDocument(created);
+ }
+ }, icon: "compress-arrows-alt"
+ }));
+ layoutItems.push({ description: ":freeform", event: () => this.props.parent.props.addDocument(Docs.Create.FreeformDocument([], { _width: 200, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" });
+ layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" });
+ layoutItems.push({ description: ":columns", event: () => this.props.parent.props.addDocument(Docs.Create.MulticolumnDocument([], { _width: 200, _height: 200 })), icon: "compress-arrows-alt" });
+ layoutItems.push({ description: ":image", event: () => this.props.parent.props.addDocument(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { _width: 200, _height: 200 })), icon: "compress-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Doc Fields ...", subitems: docItems, icon: "eye" });
+ ContextMenu.Instance.addItem({ description: "Containers ...", subitems: layoutItems, icon: "eye" });
+ ContextMenu.Instance.setDefaultItem("::", (name: string): void => {
+ Doc.GetProto(this.props.parent.props.Document)[name] = "";
+ const created = Docs.Create.TextDocument("", { title: name, _width: 250, _autoHeight: true });
+ if (created) {
+ if (this.props.parent.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document);
+ }
+ this.props.parent.props.addDocument(created);
+ }
+ });
+ const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y);
+ ContextMenu.Instance.displayMenu(pt[0], pt[1]);
+ }
+
render() {
TraceMobx();
const cols = this.props.cols();
@@ -292,7 +350,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
style={{
width: (style.columnWidth) /
((uniqueHeadings.length +
- ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ ((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1)
}}>
<div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
{/* the default bucket (no key value) has a tooltip that describes what it is.
@@ -332,7 +390,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</div>
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
- const chromeStatus = this.props.parent.props.Document.chromeStatus;
+ const chromeStatus = this.props.parent.props.Document._chromeStatus;
return (
<div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
@@ -357,7 +415,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
<div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
+ <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} />
</div> : null}
</div>
}
diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx
index 105061f46..8c7e113b2 100644
--- a/src/client/views/collections/CollectionStaffView.tsx
+++ b/src/client/views/collections/CollectionStaffView.tsx
@@ -23,10 +23,6 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) {
this.props.Document.staves = 5;
}
- @computed get fieldExtensionDoc() {
- return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- }
-
@computed get addStaffButton() {
return <div onPointerDown={this.addStaff}>+</div>;
}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4133956fc..8679c8bd1 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, trace } from "mobx";
import * as rp from 'request-promise';
import CursorField from "../../../new_fields/CursorField";
import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
@@ -40,7 +40,6 @@ export interface CollectionViewProps extends FieldViewProps {
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
- ruleProvider: Doc | undefined;
children?: never | (() => JSX.Element[]) | React.ReactNode;
isAnnotationOverlay?: boolean;
annotationsKey: string;
@@ -53,9 +52,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
private _childLayoutDisposer?: IReactionDisposer;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
- this.dropDisposer && this.dropDisposer();
- this.gestureDisposer && this.gestureDisposer();
- this.multiTouchDisposer && this.multiTouchDisposer();
+ this.dropDisposer?.();
+ this.gestureDisposer?.();
+ this.multiTouchDisposer?.();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
@@ -67,34 +66,40 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
componentDidMount() {
- this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
- async (args) => {
- if (args[1] instanceof Doc) {
- this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent"));
+ this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc)?.[Id]],
+ (args) => {
+ const childLayout = Cast(this.props.Document.childLayout, Doc);
+ if (childLayout instanceof Doc) {
+ this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layout_fromParent"));
}
- else if (!(args[1] instanceof Promise)) {
- this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout"));
+ else if (!(childLayout instanceof Promise)) {
+ this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout"));
}
- });
+ }, { fireImmediately: true });
}
componentWillUnmount() {
this._childLayoutDisposer && this._childLayoutDisposer();
}
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
// The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
// When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
// to its children which may be templates.
// If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
@computed get dataField() {
- return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
+ const { annotationsKey, fieldKey } = this.props;
+ if (annotationsKey) {
+ return this.dataDoc[fieldKey + "-" + annotationsKey];
+ }
+ return this.dataDoc[fieldKey];
}
- get childLayoutPairs() {
- return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! }));
+ get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
+ const { Document, DataDoc } = this.props;
+ const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout);
+ return validPairs.map(({ data, layout }) => ({ data: data!, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
@@ -102,7 +107,34 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
get childDocs() {
const docs = DocListCast(this.dataField);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
+ const clusters: { [key: string]: { [value: string]: string } } = {};
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ const cluster = clusters[key];
+ if (!cluster) {
+ const child: { [value: string]: string } = {};
+ child[value] = modifiers;
+ clusters[key] = child;
+ } else {
+ cluster[value] = modifiers;
+ }
+ }
+ const filteredDocs = docFilters.length ? viewedDocs.filter(d => {
+ for (const key of Object.keys(clusters)) {
+ const cluster = clusters[key];
+ const satisfiesFacet = Object.keys(cluster).some(inner => {
+ const modifier = cluster[inner];
+ return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
+ });
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : viewedDocs;
+ return filteredDocs;
}
@action
@@ -151,7 +183,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (docDragData && !docDragData.applyAsTemplate) {
if (de.altKey && docDragData.draggedDocuments.length) {
this.childDocs.map(doc =>
- Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layoutFromParent"));
+ Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layout_fromParent"));
e.stopPropagation();
return true;
}
@@ -207,7 +239,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
}
} else if (text) {
- this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }));
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 }));
}
return;
}
@@ -217,7 +249,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
if (img) {
const split = img.split("src=\"")[1].split("\"")[0];
- const doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
+ const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 });
ImageUtils.ExtractExif(doc);
this.props.addDocument(doc);
return;
@@ -232,7 +264,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
});
} else {
- const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", width: 300, height: 300, documentText: text });
+ const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text });
this.props.addDocument(htmlDoc);
}
return;
@@ -240,12 +272,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
if (text && text.indexOf("www.youtube.com/watch") !== -1) {
const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
- this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 }));
+ this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 }));
return;
}
let matches: RegExpExecArray | null;
if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
- const newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
+ const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
const proto = newBox.proto!;
const documentId = matches[2];
proto[GoogleRef] = documentId;
@@ -292,13 +324,20 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
formData.append('file', file);
const dropFileName = file ? file.name : "-empty-";
- promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
- results.map(action(({ clientAccessPath }: any) => {
- const full = { ...options, width: 300, title: dropFileName };
+ promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
+ results.map(action((result: any) => {
+ const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
+ const full = { ...options, _width: 300, title: dropFileName };
const pathname = Utils.prepend(clientAccessPath);
Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
- doc && (Doc.GetProto(doc).fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
- doc && this.props.addDocument(doc);
+ if (doc) {
+ const proto = Doc.GetProto(doc);
+ proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
+ nativeWidth && (proto["data-nativeWidth"] = nativeWidth);
+ nativeHeight && (proto["data-nativeHeight"] = nativeHeight);
+ contentSize && (proto.contentSize = contentSize);
+ this.props.addDocument(doc);
+ }
});
}));
}));
@@ -309,7 +348,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
} else {
if (text && !text.includes("https://")) {
- this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
+ this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
}
batch.end();
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 2b13d87ee..a7733ab5f 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,19 +1,22 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction } from "mobx";
+import { action, computed, observable, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
-import { emptyFunction, Utils, returnFalse, emptyPath } from '../../../Utils';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { emptyFunction, emptyPath, returnFalse, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
+import { makeTemplate } from '../../util/DropConverter';
+import { Scripting } from '../../util/Scripting';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
@@ -21,13 +24,17 @@ import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from "../EditableView";
import { MainView } from '../MainView';
+import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
+import { ImageBox } from '../nodes/ImageBox';
import { KeyValueBox } from '../nodes/KeyValueBox';
+import { ScriptBox } from '../ScriptBox';
import { Templates } from '../Templates';
-import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { CollectionViewType } from './CollectionView';
+import { RichTextField } from '../../../new_fields/RichTextField';
+import { ObjectField } from '../../../new_fields/ObjectField';
export interface TreeViewProps {
@@ -38,7 +45,6 @@ export interface TreeViewProps {
prevSibling?: Doc;
renderDepth: number;
deleteDoc: (doc: Doc) => boolean;
- ruleProvider: Doc | undefined;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean;
@@ -51,12 +57,13 @@ export interface TreeViewProps {
outdentDocument?: () => void;
ScreenToLocalTransform: () => Transform;
outerXf: () => { translateX: number, translateY: number };
- treeViewId: string;
+ treeViewId: Doc;
parentKey: string;
active: (outsideReaction?: boolean) => boolean;
hideHeaderFields: () => boolean;
preventTreeViewOpen: boolean;
renderedIds: string[];
+ onCheckedClick?: ScriptField;
}
library.add(faTrashAlt);
@@ -173,8 +180,8 @@ class TreeView extends React.Component<TreeViewProps> {
SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
OnFillDown={undoBatch((value: string) => {
Doc.SetInPlace(this.props.document, key, value, false);
- const layoutDoc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
- const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined;
+ const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
return this.props.addDocument(doc);
})}
@@ -205,7 +212,7 @@ class TreeView extends React.Component<TreeViewProps> {
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
}
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
e.stopPropagation();
@@ -232,8 +239,8 @@ class TreeView extends React.Component<TreeViewProps> {
if (inside) {
addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc);
}
- const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments);
- return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId)) || de.complete.docDragData.userDropAction) ?
+ const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments);
+ return ((de.complete.docDragData.dropAction && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ?
de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
: de.complete.docDragData.moveDocument ?
movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false)
@@ -251,21 +258,21 @@ class TreeView extends React.Component<TreeViewProps> {
}
docWidth = () => {
const layoutDoc = Doc.Layout(this.props.document);
- const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
+ const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth);
if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
- return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
+ return NumCast(layoutDoc._nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
}
docHeight = () => {
const layoutDoc = Doc.Layout(this.props.document);
const bounds = this.boundsOfCollectionDocument;
return Math.min(this.MAX_EMBED_HEIGHT, (() => {
- const aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
+ const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth, 1);
if (aspect) return this.docWidth() * aspect;
if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
- return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
- Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth,
- NumCast(this.props.containingCollection.height)))) :
- NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
+ return layoutDoc._fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) :
+ Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth,
+ NumCast(this.props.containingCollection._height)))) :
+ NumCast(layoutDoc._height) ? NumCast(layoutDoc._height) : 50;
})());
}
@@ -286,7 +293,7 @@ class TreeView extends React.Component<TreeViewProps> {
DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, doc[Id]], this.props.libraryPath);
+ [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick);
} else {
contentElement = <EditableView
key="editableView"
@@ -317,8 +324,6 @@ class TreeView extends React.Component<TreeViewProps> {
return rows;
}
- noOverlays = (doc: Doc) => ({ title: "", caption: "" });
-
@computed get renderContent() {
const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
if (expandKey !== undefined) {
@@ -331,7 +336,7 @@ class TreeView extends React.Component<TreeViewProps> {
this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath)}
+ [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
@@ -345,8 +350,6 @@ class TreeView extends React.Component<TreeViewProps> {
DataDocument={this.templateDataDoc}
LibraryPath={emptyPath}
renderDepth={this.props.renderDepth + 1}
- showOverlays={this.noOverlays}
- ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
fitToBox={this.boundsOfCollectionDocument !== undefined}
PanelWidth={this.docWidth}
PanelHeight={this.docHeight}
@@ -359,16 +362,32 @@ class TreeView extends React.Component<TreeViewProps> {
active={this.props.active}
whenActiveChanged={emptyFunction}
addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- setPreviewScript={emptyFunction} />
+ pinToPres={this.props.pinToPres} />
</div>;
}
}
+ @action
+ bulletClick = (e: React.MouseEvent) => {
+ if (this.props.onCheckedClick && this.props.document.type !== DocumentType.COL) {
+ // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check";
+ ScriptCast(this.props.onCheckedClick).script.run({
+ this: this.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document,
+ heading: this.props.containingCollection.title,
+ checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check",
+ containingTreeView: this.props.treeViewId,
+ }, console.log);
+ } else {
+ this.treeViewOpen = !this.treeViewOpen;
+ }
+ e.stopPropagation();
+ }
+
@computed
get renderBullet() {
- return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
- {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
+ const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
+ return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}>
+ {<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />}
</div>;
}
/**
@@ -377,7 +396,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderTitle() {
const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+ const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true);
const headerElements = (
<span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
@@ -427,7 +446,7 @@ class TreeView extends React.Component<TreeViewProps> {
}
public static GetChildElements(
childDocs: Doc[],
- treeViewId: string,
+ treeViewId: Doc,
containingCollection: Doc,
dataDoc: Doc | undefined,
key: string,
@@ -448,7 +467,8 @@ class TreeView extends React.Component<TreeViewProps> {
hideHeaderFields: () => boolean,
preventTreeViewOpen: boolean,
renderedIds: string[],
- libraryPath: Doc[] | undefined
+ libraryPath: Doc[] | undefined,
+ onCheckedClick: ScriptField | undefined
) {
const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -496,7 +516,7 @@ class TreeView extends React.Component<TreeViewProps> {
const rowWidth = () => panelWidth() - 20;
return docs.map((child, i) => {
- const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+ const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, child);
if (!pair.layout || pair.data instanceof Promise) {
return (null);
}
@@ -526,7 +546,7 @@ class TreeView extends React.Component<TreeViewProps> {
};
const childLayout = Doc.Layout(pair.layout);
const rowHeight = () => {
- const aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
+ const aspect = NumCast(childLayout._nativeWidth, 0) / NumCast(childLayout._nativeHeight, 0);
return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
};
return !(child instanceof Doc) ? (null) : <TreeView
@@ -536,10 +556,10 @@ class TreeView extends React.Component<TreeViewProps> {
containingCollection={containingCollection}
prevSibling={docs[i]}
treeViewId={treeViewId}
- ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc}
key={child[Id]}
indentDocument={indent}
outdentDocument={outdent}
+ onCheckedClick={onCheckedClick}
renderDepth={renderDepth}
deleteDoc={remove}
addDocument={addDocument}
@@ -608,6 +628,49 @@ export class CollectionTreeView extends CollectionSubView(Document) {
layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
+ ContextMenu.Instance.addItem({
+ description: "Buxton Layout", icon: "eye", event: () => {
+ DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
+ DocListCast(d.data).map((img, i) => {
+ const caption = (d.captions as any)[i]?.data;
+ if (caption instanceof ObjectField) {
+ Doc.GetProto(img).caption = ObjectField.MakeCopy(caption as ObjectField);
+ }
+ img._hideSidebar = true;
+ d.captions = undefined;
+ });
+ });
+ const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create;
+ const { Document } = this.props;
+ const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
+ const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
+
+ const textDoc = TextDocument("", { title: "details", _autoHeight: true });
+ const detailedLayout = Docs.Create.StackingDocument([
+ CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }),
+ textDoc,
+ ], { _chromeStatus: "disabled", title: "detailed layout stack" });
+ textDoc.data = new RichTextField(detailedTemplate, "short_description year company");
+ detailedLayout.isTemplateDoc = makeTemplate(detailedLayout);
+
+ const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
+ cardLayout.proto!.layout = ImageBox.LayoutString("hero");
+ cardLayout.showTitle = "title";
+ cardLayout.showTitleHover = "titlehover";
+
+ Document.childLayout = cardLayout;
+ Document.childDetailed = detailedLayout;
+ Document._viewType = CollectionViewType.Pivot;
+ Document.pivotField = "company";
+ }
+ });
+ const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({
+ description: "Edit onChecked Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Checked Changed ...", this.props.Document,
+ "onCheckedClick", obj.x, obj.y, { heading: "boolean", checked: "boolean", treeViewContainer: Doc.name })
+ });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
@@ -622,17 +685,17 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- const dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
+ const dropAction = StrCast(this.props.Document._dropAction) as dropActionType;
const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
<div className="collectionTreeView-dropTarget" id="body"
- style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 20)}px` }}
+ style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }}
onContextMenu={this.onContextMenu}
onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
- <EditableView
+ {(this.props.Document.treeViewHideTitle ? (null) : <EditableView
contents={this.dataDoc.title}
display={"block"}
maxHeight={72}
@@ -641,21 +704,47 @@ export class CollectionTreeView extends CollectionSubView(Document) {
SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
OnFillDown={undoBatch((value: string) => {
Doc.SetInPlace(this.dataDoc, "title", value, false);
- const layoutDoc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
- const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined;
+ const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
- })} />
+ })} />)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
- TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
+ TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields),
- BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath)
+ BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
}
</ul>
</div >
);
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey: string, facetHeader: string) {
+ const allCollectionDocs = DocListCast(dataDoc[dataKey]);
+ const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
+ set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
+
+ const facetValueDocSet = facetValues.sort().map(facetValue =>
+ Docs.Create.TextDocument("", {
+ title: facetValue.toString(),
+ treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)",
+ { layoutDoc: Doc.name, facetHeader: "string", facetValue: "string" },
+ { layoutDoc, facetHeader, facetValue })
+ }));
+ return new List<Doc>(facetValueDocSet);
+});
+
+Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
+ const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []);
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [header, value, state] = docFilters.slice(i, i + 3);
+ if (header === facetHeader && value === facetValue) {
+ return state;
+ }
+ }
+ return undefined;
+}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 88023783b..dab0ce08e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,39 +1,42 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
+import Lightbox from 'react-image-lightbox-with-rotate';
+import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
+import { DateField } from '../../../new_fields/DateField';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { StrCast, BoolCast, Cast } from '../../../new_fields/Types';
+import { listSpec } from '../../../new_fields/Schema';
+import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
+import { TraceMobx } from '../../../new_fields/util';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Utils } from '../../../Utils';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { SelectionManager } from '../../util/SelectionManager';
import { ContextMenu } from "../ContextMenu";
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { ScriptBox } from '../ScriptBox';
+import { Touchable } from '../Touchable';
import { CollectionDockingView } from "./CollectionDockingView";
import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionCarouselView } from './CollectionCarouselView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
+import { CollectionPivotView } from './CollectionPivotView';
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionStaffView } from './CollectionStaffView';
import { CollectionTreeView } from "./CollectionTreeView";
+import './CollectionView.scss';
import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
-import { CollectionLinearView } from '../CollectionLinearView';
-import { CollectionStaffView } from './CollectionStaffView';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { ImageField } from '../../../new_fields/URLField';
-import { DocListCast } from '../../../new_fields/Doc';
-import Lightbox from 'react-image-lightbox-with-rotate';
-import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
export const COLLECTION_BORDER_WIDTH = 2;
-import { DateField } from '../../../new_fields/DateField';
-import { Doc, } from '../../../new_fields/Doc';
-import { listSpec } from '../../../new_fields/Schema';
-import { DocumentManager } from '../../util/DocumentManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import './CollectionView.scss';
-import { FieldViewProps, FieldView } from '../nodes/FieldView';
-import { Touchable } from '../Touchable';
-import { TraceMobx } from '../../../new_fields/util';
-import { Utils } from '../../../Utils';
const path = require('path');
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -45,9 +48,12 @@ export enum CollectionViewType {
Tree,
Stacking,
Masonry,
+ Multicolumn,
Pivot,
+ Carousel,
Linear,
- Staff
+ Staff,
+ Timeline
}
export namespace CollectionViewType {
@@ -59,8 +65,10 @@ export namespace CollectionViewType {
["tree", CollectionViewType.Tree],
["stacking", CollectionViewType.Stacking],
["masonry", CollectionViewType.Masonry],
+ ["multicolumn", CollectionViewType.Multicolumn],
["pivot", CollectionViewType.Pivot],
- ["linear", CollectionViewType.Linear]
+ ["carousel", CollectionViewType.Carousel],
+ ["linear", CollectionViewType.Linear],
]);
export const valueOf = (value: string) => stringMapping.get(value.toLowerCase());
@@ -87,7 +95,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
get collectionViewType(): CollectionViewType | undefined {
- const viewField = Cast(this.props.Document.viewType, "number");
+ const viewField = NumCast(this.props.Document._viewType);
if (CollectionView._safeMode) {
if (viewField === CollectionViewType.Freeform) {
return CollectionViewType.Tree;
@@ -96,15 +104,15 @@ export class CollectionView extends Touchable<FieldViewProps> {
return CollectionViewType.Freeform;
}
}
- return viewField === undefined ? CollectionViewType.Invalid : viewField;
+ return viewField;
}
componentDidMount = () => {
- this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus),
+ this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus),
() => {
// chrome status is one of disabled, collapsed, or visible. this determines initial state from document
// chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
- const chromeStatus = this.props.Document.chromeStatus;
+ const chromeStatus = this.props.Document._chromeStatus;
if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
runInAction(() => this._collapsed = true);
}
@@ -114,7 +122,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
// bcz: Argh? What's the height of the collection chromes??
- chromeHeight = () => (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+ chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0);
active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
@@ -124,8 +132,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
addDocument(doc: Doc): boolean {
const targetDataDoc = Doc.GetProto(this.props.Document);
Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
- const extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
- extension && (extension.lastModified = new DateField(new Date(Date.now())));
+ targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
Doc.GetProto(doc).lastOpened = new DateField;
return true;
}
@@ -172,24 +179,26 @@ export class CollectionView extends Touchable<FieldViewProps> {
case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />);
case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
+ case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); }
case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
- case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); }
+ case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); }
case CollectionViewType.Freeform:
- default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
+ default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
}
@action
private collapse = (value: boolean) => {
this._collapsed = value;
- this.props.Document.chromeStatus = value ? "collapsed" : "enabled";
+ this.props.Document._chromeStatus = value ? "collapsed" : "enabled";
}
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
// currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
- const chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+ const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
return [chrome, this.SubViewHelper(type, renderProps)];
}
@@ -199,23 +208,25 @@ export class CollectionView extends Touchable<FieldViewProps> {
if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
const existingVm = ContextMenu.Instance.findByDescription("View Modes...");
const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
- subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
+ subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" });
if (CollectionView._safeMode) {
- ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" });
}
- subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
- subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" });
- subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+ subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" });
+ subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" });
+ subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
subItems.push({
description: "Stacking (AutoHeight)", event: () => {
- this.props.Document.viewType = CollectionViewType.Stacking;
- this.props.Document.autoHeight = true;
+ this.props.Document._viewType = CollectionViewType.Stacking;
+ this.props.Document._autoHeight = true;
}, icon: "ellipsis-v"
});
- subItems.push({ description: "Staff", event: () => this.props.Document.viewType = CollectionViewType.Staff, icon: "music" });
- subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
- subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
- switch (this.props.Document.viewType) {
+ subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" });
+ subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" });
+ subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" });
+ subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" });
+ subItems.push({ description: "Pivot", event: () => this.props.Document._viewType = CollectionViewType.Pivot, icon: "columns" });
+ switch (this.props.Document._viewType) {
case CollectionViewType.Freeform: {
subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
break;
@@ -227,12 +238,23 @@ export class CollectionView extends Touchable<FieldViewProps> {
const existing = ContextMenu.Instance.findByDescription("Layout...");
const layoutItems = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
+ if (this.props.Document.childLayout instanceof Doc) {
+ layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" });
+ }
+ if (this.props.Document.childDetailed instanceof Doc) {
+ layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" });
+ }
!existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
const more = ContextMenu.Instance.findByDescription("More...");
const moreItems = more && "subitems" in more ? more.subitems : [];
moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
!more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+
+ const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+ const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
+ !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 64411b5fe..517f467b7 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -9,8 +9,7 @@
background: lightgrey;
.collectionViewChrome {
- display: grid;
- grid-template-columns: 1fr auto;
+ display: flex;
padding-bottom: 10px;
border-bottom: .5px solid rgb(180, 180, 180);
overflow: hidden;
@@ -20,7 +19,7 @@
.collectionViewBaseChrome-viewPicker {
font-size: 75%;
- text-transform: uppercase;
+ //text-transform: uppercase;
letter-spacing: 2px;
background: rgb(238, 238, 238);
color: grey;
@@ -34,6 +33,26 @@
outline-color: black;
}
+ .collectionViewBaseChrome-cmdPicker {
+ margin-left: 3px;
+ margin-right: 0px;
+ background: rgb(238, 238, 238);
+ border: none;
+ color: grey;
+ }
+ .commandEntry-outerDiv {
+ pointer-events: all;
+ background-color: gray;
+ display: flex;
+ flex-direction: row;
+ .commandEntry-drop {
+ color:white;
+ width:25px;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ }
+
.collectionViewBaseChrome-collapse {
transition: all .5s, opacity 0.3s;
position: absolute;
@@ -53,6 +72,18 @@
.collectionViewBaseChrome-viewSpecs {
margin-left: 10px;
display: grid;
+
+ .collectionViewBaseChrome-filterIcon {
+ position: relative;
+ display: flex;
+ margin: auto;
+ background: gray;
+ color: white;
+ width: 40px;
+ height: 40px;
+ align-items: center;
+ justify-content: center;
+ }
.collectionViewBaseChrome-viewSpecsInput {
padding: 12px 10px 11px 10px;
@@ -240,7 +271,6 @@
.commandEntry-outerDiv {
display: flex;
flex-direction: column;
- width: 165px;
height: 40px;
}
.commandEntry-inputArea {
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index a870b6043..073a30330 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -39,31 +39,39 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
//(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
_templateCommand = {
- title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"],
+ title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"],
initialize: emptyFunction,
immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
};
+ _narrativeCommand = {
+ title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"],
+ initialize: emptyFunction,
+ immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
+ };
_contentCommand = {
- // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting...
- title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
+ title: "=> content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
initialize: emptyFunction,
immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
};
_viewCommand = {
- title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
- immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; },
- initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }
+ title: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
+ initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document._panX; button.restoredPanY = this.props.CollectionView.props.Document._panY; button.restoredScale = this.props.CollectionView.props.Document.scale; },
+ immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; },
};
- _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand];
+ _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand];
_stacking_commands = [this._contentCommand, this._templateCommand];
_masonry_commands = [this._contentCommand, this._templateCommand];
+ _schema_commands = [this._templateCommand, this._narrativeCommand];
_tree_commands = [];
private get _buttonizableCommands() {
switch (this.props.type) {
case CollectionViewType.Tree: return this._tree_commands;
+ case CollectionViewType.Schema: return this._schema_commands;
case CollectionViewType.Stacking: return this._stacking_commands;
case CollectionViewType.Masonry: return this._stacking_commands;
case CollectionViewType.Freeform: return this._freeform_commands;
+ case CollectionViewType.Pivot: return this._freeform_commands;
+ case CollectionViewType.Carousel: return this._freeform_commands;
}
return [];
}
@@ -126,7 +134,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
runInAction(() => {
this.addKeyRestrictions(fields);
// chrome status is one of disabled, collapsed, or visible. this determines initial state from document
- const chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
+ const chromeStatus = this.props.CollectionView.props.Document._chromeStatus;
if (chromeStatus) {
if (chromeStatus === "disabled") {
throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
@@ -143,24 +151,35 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@undoBatch
viewChanged = (e: React.ChangeEvent) => {
//@ts-ignore
- this.props.CollectionView.props.Document.viewType = parseInt(e.target.selectedOptions[0].value);
+ this.props.CollectionView.props.Document._viewType = parseInt(e.target.selectedOptions[0].value);
+ }
+
+ commandChanged = (e: React.ChangeEvent) => {
+ //@ts-ignore
+ runInAction(() => this._currentKey = e.target.selectedOptions[0].value);
}
@action
openViewSpecs = (e: React.SyntheticEvent) => {
- this._viewSpecsOpen = true;
+ if (this._viewSpecsOpen) this.closeViewSpecs();
+ else {
+ this._viewSpecsOpen = true;
- //@ts-ignore
- if (!e.target.classList[0].startsWith("qs")) {
- this.closeDatePicker();
- }
+ //@ts-ignore
+ if (!e.target?.classList[0]?.startsWith("qs")) {
+ this.closeDatePicker();
+ }
- e.stopPropagation();
- document.removeEventListener("pointerdown", this.closeViewSpecs);
- document.addEventListener("pointerdown", this.closeViewSpecs);
+ e.stopPropagation();
+ document.removeEventListener("pointerdown", this.closeViewSpecs);
+ document.addEventListener("pointerdown", this.closeViewSpecs);
+ }
}
- @action closeViewSpecs = () => { this._viewSpecsOpen = false; document.removeEventListener("pointerdown", this.closeViewSpecs); };
+ @action closeViewSpecs = () => {
+ this._viewSpecsOpen = false;
+ document.removeEventListener("pointerdown", this.closeViewSpecs);
+ };
@action
openDatePicker = (e: React.PointerEvent) => {
@@ -231,9 +250,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action
toggleCollapse = () => {
- this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled";
+ this.props.CollectionView.props.Document._chromeStatus = this.props.CollectionView.props.Document._chromeStatus === "enabled" ? "collapsed" : "enabled";
if (this.props.collapse) {
- this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled");
+ this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled");
}
}
@@ -261,7 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@observable private pivotKeyDisplay = this.pivotKey;
getPivotInput = () => {
- if (StrCast(this.document.freeformLayoutEngine) !== "pivot") {
+ if (StrCast(this.document._freeformLayoutEngine) !== "pivot") {
return (null);
}
return (<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -372,7 +391,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
render() {
- const collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
+ const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
return (
<div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
<div className="collectionViewChrome">
@@ -391,23 +410,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
className="collectionViewBaseChrome-viewPicker"
onPointerDown={stopPropagation}
onChange={this.viewChanged}
- value={NumCast(this.props.CollectionView.props.Document.viewType)}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option>
+ value={NumCast(this.props.CollectionView.props.Document._viewType)}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">schema</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Multicolumn</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Pivot</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Carousel</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Linear</option>
</select>
- <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
- <input className="collectionViewBaseChrome-viewSpecsInput"
- placeholder="FILTER"
- value={this.filterValue ? this.filterValue.script.originalScript === "return true" ? "" : this.filterValue.script.originalScript : ""}
- onChange={(e) => { }}
- onPointerDown={this.openViewSpecs}
- id="viewSpecsInput" />
- {this.getPivotInput()}
+ <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}>
+ <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} >
+ <FontAwesomeIcon icon="filter" size="2x" />
+ </div>
<div className="collectionViewBaseChrome-viewSpecsMenu"
onPointerDown={this.openViewSpecs}
style={{
@@ -448,17 +465,20 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
</div>
</div>
<div className="collectionViewBaseChrome-template" ref={this.createDropTarget} >
- <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
- <div className="commandEntry-inputArea" onPointerDown={this.autoSuggestDown} >
- <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
- getSuggestionValue={this.getSuggestionValue}
- suggestions={this.suggestions}
- alwaysRenderSuggestions={true}
- renderSuggestion={this.renderSuggestion}
- onSuggestionsFetchRequested={this.onSuggestionFetch}
- onSuggestionsClearRequested={this.onSuggestionClear}
- ref={this._autosuggestRef} />
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
+ <div className="commandEntry-drop">
+ <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
</div>
+ <select
+ className="collectionViewBaseChrome-cmdPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.commandChanged}
+ value={this._currentKey}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option>
+ {this._buttonizableCommands.map(cmd =>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
+ )}
+ </select>
</div>
</div>
</div>
@@ -599,15 +619,6 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
return (
<div className="collectionSchemaViewChrome-cont">
<div className="collectionSchemaViewChrome-toggle">
- <div className="collectionSchemaViewChrome-label">Wrap Text: </div>
- <div className="collectionSchemaViewChrome-toggler" onClick={this.toggleTextwrap}>
- <div className={"collectionSchemaViewChrome-togglerButton" + (textWrapped ? " on" : " off")}>
- {textWrapped ? "on" : "off"}
- </div>
- </div>
- </div>
-
- <div className="collectionSchemaViewChrome-toggle">
<div className="collectionSchemaViewChrome-label">Show Preview: </div>
<div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}>
<div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}>
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
index d293bb5ca..a266861bd 100644
--- a/src/client/views/collections/ParentDocumentSelector.scss
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -35,8 +35,6 @@
pointer-events: all;
position: relative;
display: inline-block;
- padding-left: 5px;
- padding-right: 5px;
}
.parentDocumentSelector-metadata {
pointer-events: auto;
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 24aa6ddfa..115f8d633 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -6,7 +6,7 @@ import { observable, action, runInAction } from "mobx";
import { Id } from "../../../new_fields/FieldSymbols";
import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
-import { NumCast } from "../../../new_fields/Types";
+import { NumCast, StrCast } from "../../../new_fields/Types";
import { CollectionViewType } from "./CollectionView";
import { DocumentButtonBar } from "../DocumentButtonBar";
import { DocumentManager } from "../../util/DocumentManager";
@@ -21,7 +21,13 @@ export const Flyout = higflyout.default;
library.add(faEdit);
-type SelectorProps = { Document: Doc, Views: DocumentView[], Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
+type SelectorProps = {
+ Document: Doc,
+ Views: DocumentView[],
+ Stack?: any,
+ addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void
+};
+
@observer
export class SelectorContextMenu extends React.Component<SelectorProps> {
@observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -49,31 +55,22 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
getOnClick({ col, target }: { col: Doc, target: Doc }) {
return () => {
col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
- if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
- const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
- col.panX = newPanX;
- col.panY = newPanY;
+ if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target._width) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target._height) / 2;
+ col._panX = newPanX;
+ col._panY = newPanY;
}
this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc?
};
}
- get metadataMenu() {
- return <div className="parentDocumentSelector-metadata">
- <Flyout anchorPoint={anchorPoints.TOP_LEFT}
- content={<MetadataEntryMenu docs={() => this.props.Views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
- <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
- </Flyout>
- </div>;
- }
render() {
return <div >
- <div key="metadata">Metadata: {this.metadataMenu}</div>
<p key="contexts">Contexts:</p>
- {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)}
{this._otherDocs.length ? <hr key="hr" /> : null}
- {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._otherDocs.map(doc => <p key={"p" + doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title?.toString()}</a></p>)}
</div>;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 012115b1f..be1317b25 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -7,8 +7,9 @@ import { OverlayView, OverlayElementOptions } from "../../OverlayView";
import { emptyFunction } from "../../../../Utils";
import React = require("react");
import { ObservableMap, runInAction } from "mobx";
-import { Id } from "../../../../new_fields/FieldSymbols";
-import { DateField } from "../../../../new_fields/DateField";
+import { Id, ToString } from "../../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { RefField } from "../../../../new_fields/RefField";
interface PivotData {
type: string;
@@ -35,21 +36,19 @@ export interface ViewDefResult {
}
function toLabel(target: FieldResult<Field>) {
- if (target instanceof DateField) {
- const date = DateCast(target).date;
- if (date) {
- return `${date.toDateString()} ${date.toTimeString()}`;
- }
+ if (target instanceof ObjectField || target instanceof RefField) {
+ return target[ToString]();
}
return String(target);
}
-export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
+export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+ const pivotFieldKey = toLabel(pivotDoc.pivotField);
for (const doc of childDocs) {
- const val = doc[StrCast(pivotDoc.pivotField, "title")];
+ const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
!pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
pivotColumnGroups.get(val)!.push(doc);
@@ -57,9 +56,10 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
}
const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity);
- const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
+ let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
const docMap = new Map<Doc, ViewDefBounds>();
const groupNames: PivotData[] = [];
+ numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols);
const expander = 1.05;
const gap = .15;
@@ -73,26 +73,26 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
x,
y: pivotAxisWidth + 50,
width: pivotAxisWidth * expander * numCols,
- height: 100,
+ height: NumCast(pivotDoc.pivotFontSize, 10),
fontSize: NumCast(pivotDoc.pivotFontSize, 10)
});
for (const doc of val) {
const layoutDoc = Doc.Layout(doc);
let wid = pivotAxisWidth;
- let hgt = layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
+ let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
if (hgt > pivotAxisWidth) {
hgt = pivotAxisWidth;
- wid = layoutDoc.nativeHeight ? (NumCast(layoutDoc.nativeWidth) / NumCast(layoutDoc.nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
+ wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
- x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2,
+ x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
y: -y,
width: wid,
height: hgt
});
xCount++;
if (xCount >= numCols) {
- xCount = (pivotAxisWidth - wid) / 2;
+ xCount = 0;
y += pivotAxisWidth * expander;
}
}
@@ -104,8 +104,8 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
x: NumCast(pair.layout.x),
y: NumCast(pair.layout.y),
z: NumCast(pair.layout.z),
- width: NumCast(pair.layout.width),
- height: NumCast(pair.layout.height)
+ width: NumCast(pair.layout._width),
+ height: NumCast(pair.layout._height)
};
const pos = docMap.get(pair.layout) || defaultPosition;
const data = poolData.get(pair.layout[Id]);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 178a5bcdc..b8fbaef5c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -54,8 +54,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
} else {
setTimeout(() => {
(this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc);
- let m = targetBhyperlink.getBoundingClientRect();
- let mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
+ const m = targetBhyperlink.getBoundingClientRect();
+ const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100;
this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100;
}, 0);
@@ -66,8 +66,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
} else {
setTimeout(() => {
(this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc);
- let m = targetAhyperlink.getBoundingClientRect();
- let mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
+ const m = targetAhyperlink.getBoundingClientRect();
+ const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100;
this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100;
}, 0);
@@ -93,8 +93,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
apt.point.x, apt.point.y);
const pt1 = [apt.point.x, apt.point.y];
const pt2 = [bpt.point.x, bpt.point.y];
- let aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
- let bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
+ const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
+ const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
return !aActive && !bActive ? (null) :
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
style={{ opacity: this._opacity, strokeDasharray: "2 2" }}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2622d9b28..8371d0491 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -3,13 +3,13 @@ import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc";
import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
+import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
@@ -26,7 +26,6 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
-import { CreatePolyline } from "../../InkingStroke";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
@@ -42,21 +41,20 @@ import React = require("react");
import { computedFn } from "mobx-utils";
import { TraceMobx } from "../../../../new_fields/util";
import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
-import { LinkManager } from "../../../util/LinkManager";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import CollectionPaletteVIew from "../../Palette";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
export const panZoomSchema = createSchema({
- panX: "number",
- panY: "number",
+ _panX: "number",
+ _panY: "number",
scale: "number",
arrangeScript: ScriptField,
arrangeInit: ScriptField,
useClusters: "boolean",
- isRuleProvider: "boolean",
fitToBox: "boolean",
+ xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
panTransformType: "string",
scrollHeight: "number",
fitX: "number",
@@ -77,20 +75,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _layoutComputeReaction: IReactionDisposer | undefined;
private _layoutPoolData = new ObservableMap<string, any>();
- public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
+ public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive
@observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
@observable _clusterSets: (Doc[])[] = [];
- @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
+ @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
- @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
- @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
+ @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc.xPadding, 10), NumCast(this.layoutDoc.yPadding, 10)); }
+ @computed get nativeWidth() { return this.Document._fitToContent ? 0 : NumCast(this.Document._nativeWidth); }
+ @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight); }
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
private easing = () => this.props.Document.panTransformType === "Ease";
- private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
- private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0;
+ private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
+ private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0;
private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
this.Document.scale || 1)
@@ -102,14 +100,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
- const maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
- let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0;
- if (heading === 0) {
- const sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
- DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0);
- heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
- }
- !this.Document.isRuleProvider && (newBox.heading = heading);
this.addDocument(newBox);
}
private addDocument = (newBox: Doc) => {
@@ -154,18 +144,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const layoutDoc = Doc.Layout(d);
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(layoutDoc.width)) {
- layoutDoc.width = 300;
+ if (!NumCast(layoutDoc._width)) {
+ layoutDoc._width = 300;
}
- if (!NumCast(layoutDoc.height)) {
- const nw = NumCast(layoutDoc.nativeWidth);
- const nh = NumCast(layoutDoc.nativeHeight);
- layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
+ if (!NumCast(layoutDoc._height)) {
+ const nw = NumCast(layoutDoc._nativeWidth);
+ const nh = NumCast(layoutDoc._nativeHeight);
+ layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300;
}
this.bringToFront(d);
}));
- de.complete.docDragData.droppedDocuments.length === 1 && this.updateCluster(de.complete.docDragData.droppedDocuments[0]);
+ (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments);
}
}
else if (de.complete.annoDragData) {
@@ -190,8 +180,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const layoutDoc = Doc.Layout(cd);
const cx = NumCast(cd.x) - this._clusterDistance;
const cy = NumCast(cd.y) - this._clusterDistance;
- const cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
- const ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+ const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
+ const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
NumCast(cd.cluster) : cluster;
}, -1);
@@ -223,6 +213,41 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
}
+ @action
+ updateClusterDocs(docs: Doc[]) {
+ const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
+ if (this.props.Document.useClusters) {
+ const docFirst = docs[0];
+ docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
+ const preferredInd = NumCast(docFirst.cluster);
+ docs.map(doc => doc.cluster = -1);
+ docs.map(doc => this._clusterSets.map((set, i) => set.map(member => {
+ if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+ docFirst.cluster = i;
+ }
+ })));
+ if (docFirst.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+ docFirst.cluster = preferredInd;
+ }
+ this._clusterSets.map((set, i) => {
+ if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
+ docFirst.cluster = i;
+ }
+ });
+ if (docFirst.cluster === -1) {
+ docs.map(doc => {
+ doc.cluster = this._clusterSets.length;
+ this._clusterSets.push([doc]);
+ });
+ } else {
+ for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc));
+ }
+ childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child));
+ childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString());
+ }
+ }
+
@undoBatch
@action
updateCluster(doc: Doc) {
@@ -280,7 +305,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return;
}
this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
- if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
+ if (e.button === 0 && (!e.shiftKey || this._hitCluster) && !e.altKey && !e.ctrlKey && this.props.active(true)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -327,7 +352,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
if (!e.nativeEvent.cancelBubble) {
- // const myTouches = InteractionUtils.GetMyTargetTouches(e, this.prevPoints, true);
+ // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
const pt = me.changedTouches[0];
if (pt) {
this._hitCluster = this.props.Document.useCluster ? this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)) !== -1 : false;
@@ -346,6 +371,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this._lastX = pt.pageX;
this._lastY = pt.pageY;
e.preventDefault();
+ e.stopPropagation();
}
else {
e.preventDefault();
@@ -361,7 +387,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
case GestureUtils.Gestures.Stroke:
const points = ge.points;
const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
- const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { x: B.x, y: B.y, width: B.width, height: B.height });
+ const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, parseInt(InkingControl.Instance.selectedWidth), points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height });
this.addDocument(inkDoc);
e.stopPropagation();
break;
@@ -383,10 +409,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
return pass;
});
- this.addDocument(Docs.Create.FreeformDocument(sel, { x: bounds.x, y: bounds.y, width: bWidth, height: bHeight, panX: 0, panY: 0 }));
+ this.addDocument(Docs.Create.FreeformDocument(sel, { title: "nested collection", x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 }));
sel.forEach(d => this.props.removeDocument(d));
break;
-
+ case GestureUtils.Gestures.Text:
+ if (ge.text) {
+ const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y);
+ this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] }));
+ }
}
}
@@ -405,22 +435,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// I think it makes sense for the marquee menu to go away when panned. -syip2
MarqueeOptionsMenu.Instance.fadeOut(true);
- let x = this.Document.panX || 0;
- let y = this.Document.panY || 0;
- const docs = this.childLayoutPairs.map(pair => pair.layout);
+ let x = this.Document._panX || 0;
+ let y = this.Document._panY || 0;
+ const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout);
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- if (!this.isAnnotationOverlay) {
+ if (!this.isAnnotationOverlay && docs.length) {
PDFMenu.Instance.fadeOut(true);
- const minx = docs.length ? NumCast(docs[0].x) : 0;
- const maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
- const miny = docs.length ? NumCast(docs[0].y) : 0;
- const maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
+ const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
+ const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx;
+ const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny;
const ranges = docs.filter(doc => doc).reduce((range, doc) => {
- const layoutDoc = Doc.Layout(doc);
- const x = NumCast(doc.x);
- const xe = x + NumCast(layoutDoc.width);
- const y = NumCast(doc.y);
- const ye = y + NumCast(layoutDoc.height);
+ const x = this.childDataProvider(doc).x;//NumCast(doc.x);
+ const y = this.childDataProvider(doc).y;//NumCast(doc.y);
+ const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width);
+ const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.height);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
@@ -600,8 +629,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
- this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ this.Document._panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.Document._panY = this.isAnnotationOverlay ? newPanY : panY;
}
}
@@ -625,14 +654,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// TODO This technically isn't correct if type !== "doc", as
// currently nothing is done, but we should probably push a new state
- if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) {
+ if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) {
const init = state.initializers![this.Document[Id]];
if (!init) {
- state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY };
+ state.initializers![this.Document[Id]] = { panX: this.Document._panX, panY: this.Document._panY };
HistoryUtil.pushState(state);
- } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) {
- init.panX = this.Document.panX;
- init.panY = this.Document.panY;
+ } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) {
+ init.panX = this.Document._panX;
+ init.panY = this.Document._panY;
HistoryUtil.pushState(state);
}
}
@@ -648,13 +677,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
} else {
const layoutdoc = Doc.Layout(doc);
- const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
+ const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2;
const newState = HistoryUtil.getState();
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
- const savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+ const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
if (!doc.z) this.setPan(newPanX, newPanY, "Ease"); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
Doc.BrushDoc(this.props.Document);
@@ -664,8 +693,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
afterFocus && setTimeout(() => {
if (afterFocus && afterFocus()) {
- this.Document.panX = savedState.px;
- this.Document.panY = savedState.py;
+ this.Document._panX = savedState.px;
+ this.Document._panY = savedState.py;
this.Document.scale = savedState.s;
this.Document.panTransformType = savedState.pt;
}
@@ -675,7 +704,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
- this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
+ this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
}
zoomToScale = (scale: number) => {
@@ -685,6 +714,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getScale = () => this.Document.scale || 1;
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
@@ -693,8 +723,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
Document: childLayout,
LibraryPath: this.libraryPath,
layoutKey: undefined,
- ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
- onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ //onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
+ onClick: this.onChildClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
PanelWidth: childLayout[WidthSym],
@@ -717,7 +747,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return { ...result, transition: "transform 1s" };
}
const layoutDoc = Doc.Layout(params.doc);
- return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
+ return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") };
}
viewDefsToJSX = (views: any[]) => {
@@ -735,7 +765,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const fontSize = Cast(viewDef.fontSize, "number");
return [text, x, y, width, height].some(val => val === undefined) ? undefined :
{
- ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
+ ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
{text}
</div>,
bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
@@ -743,11 +773,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
- childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) { return this._layoutPoolData.get(doc[Id]); }.bind(this));
+ childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) {
+ if (!doc) {
+ console.log(doc);
+ }
+ return this._layoutPoolData.get(doc[Id]);
+ }.bind(this));
doPivotLayout(poolData: ObservableMap<string, any>) {
return computePivotLayout(poolData, this.props.Document, this.childDocs,
- this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
doFreeformLayout(poolData: ObservableMap<string, any>) {
@@ -769,15 +804,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
get doLayoutComputation() {
let computedElementData: { elements: ViewDefResult[] };
- switch (this.Document.freeformLayoutEngine) {
+ switch (this.Document._freeformLayoutEngine) {
case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break;
default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break;
}
this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair =>
computedElementData.elements.push({
- ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.childDataProvider}
- ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
- jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
+ dataProvider={this.childDataProvider}
+ jitterRotation={NumCast(this.props.Document.jitterRotation)}
+ fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />,
bounds: this.childDataProvider(pair.layout)
}));
@@ -804,12 +840,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
layoutDocsInGrid = () => {
UndoManager.RunInBatch(() => {
const docs = DocListCast(this.Document[this.props.fieldKey]);
- const startX = this.Document.panX || 0;
+ const startX = this.Document._panX || 0;
let x = startX;
- let y = this.Document.panY || 0;
+ let y = this.Document._panY || 0;
let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc.width)));
- const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ const width = Math.max(...docs.map(doc => NumCast(doc._width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc._height)));
for (const doc of docs) {
doc.x = x;
doc.y = y;
@@ -823,37 +859,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}, "arrange contents");
}
- autoFormat = () => {
- this.Document.isRuleProvider = !this.Document.isRuleProvider;
- // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading.
- this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
- // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
- DocListCast(Doc.Layout(pair.layout).data).map(heading => {
- const headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
- const headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
- if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
- Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
- }
- })
- );
- }
-
- analyzeStrokes = async () => {
- const children = await DocListCastAsync(this.dataDoc.data);
- if (!children) {
- return;
- }
- const inkData: InkData[] = [];
- for (const doc of children) {
- const data = Cast(doc.data, InkField)?.inkData;
- data && inkData.push(data);
- }
- if (!inkData.length) {
- return;
- }
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], inkData);
- }
-
private thumbIdentifier?: number;
// @action
@@ -900,16 +905,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
onContextMenu = (e: React.MouseEvent) => {
const layoutItems: ContextMenuProps[] = [];
- if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
- layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
- }
- layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: `${this.Document.LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document.LODdisable = !this.Document.LODdisable, icon: "table" });
- layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" });
+ layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
- layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" });
layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
- layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+ // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
layoutItems.push({
description: "Import document", icon: "upload", event: ({ x, y }) => {
@@ -943,7 +944,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
description: "Add Note ...",
subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
description: (i + 1) + ": " + StrCast(note.title),
- event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
+ event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })),
icon: "eye"
})) as ContextMenuProps[],
icon: "eye"
@@ -964,7 +965,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
children = () => {
const eles: JSX.Element[] = [];
- this.extensionDoc && (eles.push(...this.childViews()));
+ eles.push(...this.childViews());
// this._palette && (eles.push(this._palette));
// this.currentStroke && (eles.push(this.currentStroke));
eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
@@ -972,11 +973,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
@computed get placeholder() {
return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}>
- <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title}</span>
+ <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span>
</div>;
}
@computed get marqueeView() {
- return <MarqueeView {...this.props} extensionDoc={this.extensionDoc!} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+ return <MarqueeView {...this.props} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
@@ -984,6 +985,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
</CollectionFreeFormViewPannableContents>
</MarqueeView>;
}
+
+ @computed get contentScaling() {
+ if (this.props.annotationsKey) return 0;
+ const hscale = this.nativeHeight ? this.props.PanelHeight() / this.nativeHeight : 1;
+ const wscale = this.nativeWidth ? this.props.PanelWidth() / this.nativeWidth : 1;
+ return wscale < hscale ? wscale : hscale;
+ }
render() {
TraceMobx();
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
@@ -993,12 +1001,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
// if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
- if (!this.extensionDoc) return (null);
// let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
- return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
- style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
- {!this.Document.LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
+ return <div className={"collectionfreeformview-container"}
+ ref={this.createDashEventsTarget}
+ onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}
+ style={{
+ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ transform: this.contentScaling ? `scale(${this.contentScaling})` : "",
+ transformOrigin: this.contentScaling ? "left top" : "",
+ width: this.contentScaling ? `${100 / this.contentScaling}%` : "",
+ height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
+ }}>
+ {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
this.placeholder : this.marqueeView}
<CollectionFreeFormOverlayView elements={this.elementFunc} />
</div>;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 32e39d25e..db4b674b5 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -11,6 +11,7 @@ export default class MarqueeOptionsMenu extends AntimodeMenu {
public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
+ public inkToText: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
public hideMarquee: () => void = unimplementedFunction;
@@ -25,21 +26,31 @@ export default class MarqueeOptionsMenu extends AntimodeMenu {
<button
className="antimodeMenu-button"
title="Create a Collection"
+ key="group"
onPointerDown={this.createCollection}>
<FontAwesomeIcon icon="object-group" size="lg" />
</button>,
<button
className="antimodeMenu-button"
title="Summarize Documents"
+ key="summarize"
onPointerDown={this.summarize}>
<FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
</button>,
<button
className="antimodeMenu-button"
title="Delete Documents"
+ key="delete"
onPointerDown={this.delete}>
<FontAwesomeIcon icon="trash-alt" size="lg" />
</button>,
+ <button
+ className="antimodeMenu-button"
+ title="Change to Text"
+ key="inkToText"
+ onPointerDown={this.inkToText}>
+ <FontAwesomeIcon icon="font" size="lg" />
+ </button>,
];
return this.getElement(buttons);
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 523edb918..112df8f05 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,12 +1,12 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc";
import { InkField } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
import { listSpec } from "../../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
import { ComputedField } from "../../../../new_fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../../Utils";
import { Docs } from "../../../documents/Documents";
@@ -19,6 +19,7 @@ import "./MarqueeView.scss";
import React = require("react");
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { SubCollectionViewProps } from "../CollectionSubView";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -29,7 +30,6 @@ interface MarqueeViewProps {
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
- extensionDoc: Doc;
isAnnotationOverlay?: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@@ -85,7 +85,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
ns.map(line => {
const indent = line.search(/\S|$/);
- const newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ const newBox = Docs.Create.TextDocument(line, { _width: 200, _height: 35, x: x + indent / 3 * 10, y: y, title: line });
this.props.addDocument(newBox);
y += 40 * this.props.getTransform().Scale;
});
@@ -95,17 +95,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
navigator.clipboard.readText().then(text => {
const ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
if (ns.length === 1 && text.startsWith("http")) {
- this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
+ this.props.addDocument(Docs.Create.ImageDocument(text, { _nativeWidth: 300, _width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
} else {
this.pasteTable(ns, x, y);
}
});
} else if (!e.ctrlKey) {
this.props.addLiveTextDocument(
- Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
+ Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
} else if (e.keyCode > 48 && e.keyCode <= 57) {
const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
- const text = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" });
+ const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
text.layout = notes[(e.keyCode - 49) % notes.length];
this.props.addLiveTextDocument(text);
}
@@ -128,7 +128,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
let groupAttr: string | number = "";
const rowProto = new Doc();
rowProto.title = rowProto.Id;
- rowProto.width = 200;
+ rowProto._width = 200;
rowProto.isPrototype = true;
for (let i = 1; i < ns.length - 1; i++) {
const values = ns[i].split("\t");
@@ -144,10 +144,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
docDataProto.title = i.toString();
const doc = Doc.MakeDelegate(docDataProto);
- doc.width = 200;
+ doc._width = 200;
docList.push(doc);
}
- const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+ const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", _width: 300, _height: 100 });
this.props.addDocument(newCol);
}
@@ -205,6 +205,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
+ MarqueeOptionsMenu.Instance.inkToText = this.syntaxHighlight;
MarqueeOptionsMenu.Instance.showMarquee = this.showMarquee;
MarqueeOptionsMenu.Instance.hideMarquee = this.hideMarquee;
MarqueeOptionsMenu.Instance.jumpTo(e.clientX, e.clientY);
@@ -267,15 +268,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
get inkDoc() {
- return this.props.extensionDoc;
+ return this.props.Document;
}
get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
- return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
+ return Cast(this.props.Document.ink, InkField);
}
set ink(value: InkField | undefined) {
- this.props.extensionDoc && (this.props.extensionDoc.ink = value);
+ this.props.Document.ink = value;
}
@action
@@ -300,7 +301,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.hideMarquee();
}
- getCollection = (selected: Doc[]) => {
+ getCollection = (selected: Doc[], asTemplate: boolean) => {
const bounds = this.Bounds;
const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
"rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
@@ -322,15 +323,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
// const inkData = this.ink ? this.ink.inkData : undefined;
- const newCollection = Docs.Create.FreeformDocument(selected, {
+ const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument;
+ const newCollection = creator(selected, {
x: bounds.left,
y: bounds.top,
- panX: 0,
- panY: 0,
+ _panX: 0,
+ _panY: 0,
backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
- width: bounds.width,
- height: bounds.height,
+ _width: bounds.width,
+ _height: bounds.height,
+ _LODdisable: true,
title: "a nested collection",
});
// const dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
@@ -353,7 +356,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
return d;
});
}
- const newCollection = this.getCollection(selected);
+ const newCollection = this.getCollection(selected, e.key === "t");
this.props.addDocument(newCollection);
this.props.selectDocuments([newCollection], []);
MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -361,6 +364,29 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
@action
+ syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const selected = this.marqueeSelect(false);
+ if (e instanceof KeyboardEvent ? e.key === "i" : true) {
+ const inks = selected.filter(s => s.proto?.type === "ink");
+ const sets = selected.filter(s => s.proto?.type === "text")
+ const inkFields = inks.map(i => FieldValue(Cast(i.data, InkField)));
+ CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => {
+ const wordResults = results.filter((r: any) => r.category === "inkWord");
+ console.log(wordResults);
+ for (const word of wordResults) {
+ const indices: number[] = word.strokeIds;
+ const r = Math.floor(Math.random() * 256);
+ const g = Math.floor(Math.random() * 256);
+ const b = Math.floor(Math.random() * 256);
+ indices.forEach(i => {
+ inks[i].color = `rgb(${r}, ${g}, ${b})`;
+ })
+ }
+ });
+ }
+ }
+
+ @action
summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const bounds = this.Bounds;
const selected = this.marqueeSelect(false);
@@ -373,16 +399,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.page = -1;
return d;
});
- newCollection.chromeStatus = "disabled";
- const summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+ newCollection._chromeStatus = "disabled";
+ const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
newCollection.x = bounds.left + bounds.width;
Doc.GetProto(newCollection).summaryDoc = summary;
Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
- const container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
- container.viewType = CollectionViewType.Stacking;
- container.autoHeight = true;
+ const container = Docs.Create.FreeformDocument([summary, newCollection], {
+ x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true,
+ _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-"
+ });
Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight"
this.props.addLiveTextDocument(container);
} else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
@@ -405,12 +432,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.delete();
e.stopPropagation();
}
- if (e.key === "c" || e.key === "s" || e.key === "S") {
+ if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
(e as any).propagationIsStopped = true;
- if (e.key === "c") {
+ if (e.key === "c" || e.key === "t") {
this.collection(e);
}
@@ -467,8 +494,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const layoutDoc = Doc.Layout(doc);
const x = NumCast(doc.x);
const y = NumCast(doc.y);
- const w = NumCast(layoutDoc.width);
- const h = NumCast(layoutDoc.height);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
@@ -478,8 +505,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const layoutDoc = Doc.Layout(doc);
const x = NumCast(doc.x);
const y = NumCast(doc.y);
- const w = NumCast(layoutDoc.width);
- const h = NumCast(layoutDoc.height);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
@@ -495,8 +522,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const layoutDoc = Doc.Layout(doc);
const x = NumCast(doc.x);
const y = NumCast(doc.y);
- const w = NumCast(layoutDoc.width);
- const h = NumCast(layoutDoc.height);
+ const w = NumCast(layoutDoc._width);
+ const h = NumCast(layoutDoc._height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
selection.push(doc);
}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
new file mode 100644
index 000000000..f57ba438a
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -0,0 +1,33 @@
+.collectionMulticolumnView_contents {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ .document-wrapper {
+ display: flex;
+ flex-direction: column;
+
+ .label-wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ height: 20px;
+ }
+
+ }
+
+ .resizer {
+ cursor: ew-resize;
+ transition: 0.5s opacity ease;
+ display: flex;
+ flex-direction: column;
+
+ .internal {
+ width: 100%;
+ height: 100%;
+ transition: 0.5s background-color ease;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
new file mode 100644
index 000000000..041eb69da
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -0,0 +1,258 @@
+import { observer } from 'mobx-react';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
+import * as React from "react";
+import { Doc } from '../../../../new_fields/Doc';
+import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { Utils } from '../../../../Utils';
+import "./collectionMulticolumnView.scss";
+import { computed, trace, observable, action } from 'mobx';
+import { Transform } from '../../../util/Transform';
+import WidthLabel from './MulticolumnWidthLabel';
+import ResizeBar from './MulticolumnResizer';
+import { undoBatch } from '../../../util/UndoManager';
+import { DragManager } from '../../../util/DragManager';
+
+type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
+const MulticolumnDocument = makeInterface(documentSchema);
+
+interface WidthSpecifier {
+ magnitude: number;
+ unit: string;
+}
+
+interface LayoutData {
+ widthSpecifiers: WidthSpecifier[];
+ starSum: number;
+}
+
+export const WidthUnit = {
+ Pixel: "px",
+ Ratio: "*"
+};
+
+const resolvedUnits = Object.values(WidthUnit);
+const resizerWidth = 4;
+
+@observer
+export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
+
+ /**
+ * @returns the list of layout documents whose width unit is
+ * *, denoting that it will be displayed with a ratio, not fixed pixel, value
+ */
+ @computed
+ private get ratioDefinedDocs() {
+ return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio);
+ }
+
+ /**
+ * This loops through all childLayoutPairs and extracts the values for widthUnit
+ * and widthMagnitude, ignoring any that are malformed. Additionally, it then
+ * normalizes the ratio values so that one * value is always 1, with the remaining
+ * values proportionate to that easily readable metric.
+ * @returns the list of the resolved width specifiers (unit and magnitude pairs)
+ * as well as the sum of the * coefficients, i.e. the ratio magnitudes
+ */
+ @computed
+ private get resolvedLayoutInformation(): LayoutData {
+ let starSum = 0;
+ const widthSpecifiers: WidthSpecifier[] = [];
+ this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => {
+ const unit = StrCast(widthUnit);
+ const magnitude = NumCast(widthMagnitude);
+ if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
+ (unit === WidthUnit.Ratio) && (starSum += magnitude);
+ widthSpecifiers.push({ magnitude, unit });
+ }
+ /**
+ * Otherwise, the child document is ignored and the remaining
+ * space is allocated as if the document were absent from the child list
+ */
+ });
+
+ /**
+ * Here, since these values are all relative, adjustments during resizing or
+ * manual updating can, though their ratios remain the same, cause the values
+ * themselves to drift toward zero. Thus, whenever we change any of the values,
+ * we normalize everything (dividing by the smallest magnitude).
+ */
+ setTimeout(() => {
+ const { ratioDefinedDocs } = this;
+ if (this.childLayoutPairs.length) {
+ const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
+ if (minimum !== 0) {
+ ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+ }
+ }
+ });
+
+ return { widthSpecifiers, starSum };
+ }
+
+ /**
+ * This returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with higher priority) requested a fixed pixel width.
+ *
+ * If the underlying resolvedLayoutInformation returns null
+ * because we're waiting on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalFixedAllocation(): number | undefined {
+ return this.resolvedLayoutInformation?.widthSpecifiers.reduce(
+ (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0);
+ }
+
+ /**
+ * @returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with lower priority) requested a certain relative proportion of the
+ * remaining pixel width not allocated for fixed widths.
+ *
+ * If the underlying totalFixedAllocation returns undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalRatioAllocation(): number | undefined {
+ const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length;
+ if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
+ return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
+ }
+ }
+
+ /**
+ * @returns the total quantity, in pixels, that
+ * 1* (relative / star unit) is worth. For example,
+ * if the configuration has three documents, with, respectively,
+ * widths of 2*, 2* and 1*, and the panel width returns 1000px,
+ * this accessor returns 1000 / (2 + 2 + 1), or 200px.
+ * Elsewhere, this is then multiplied by each relative-width
+ * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px).
+ *
+ * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get columnUnitLength(): number | undefined {
+ if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) {
+ return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum;
+ }
+ }
+
+ /**
+ * This wrapper function exists to prevent mobx from
+ * needlessly rerendering the internal ContentFittingDocumentViews
+ */
+ private getColumnUnitLength = () => this.columnUnitLength;
+
+ /**
+ * @param layout the document whose transform we'd like to compute
+ * Given a layout document, this function
+ * returns the resolved width it has requested, in pixels.
+ * @returns the stored column width if already in pixels,
+ * or the ratio width evaluated to a pixel value
+ */
+ private lookupPixels = (layout: Doc): number => {
+ const columnUnitLength = this.columnUnitLength;
+ if (columnUnitLength === undefined) {
+ return 0; // we're still waiting on promises to resolve
+ }
+ let width = NumCast(layout.widthMagnitude);
+ if (StrCast(layout.widthUnit) === WidthUnit.Ratio) {
+ width *= columnUnitLength;
+ }
+ return width;
+ }
+
+ /**
+ * @returns the transform that will correctly place
+ * the document decorations box, shifted to the right by
+ * the sum of all the resolved column widths of the
+ * documents before the target.
+ */
+ private lookupIndividualTransform = (layout: Doc) => {
+ const columnUnitLength = this.columnUnitLength;
+ if (columnUnitLength === undefined) {
+ return Transform.Identity(); // we're still waiting on promises to resolve
+ }
+ let offset = 0;
+ for (const { layout: candidate } of this.childLayoutPairs) {
+ if (candidate === layout) {
+ return this.props.ScreenToLocalTransform().translate(-offset, 0);
+ }
+ offset += this.lookupPixels(candidate) + resizerWidth;
+ }
+ return Transform.Identity(); // type coersion, this case should never be hit
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.drop(e, de)) {
+ de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => {
+ d.widthUnit = "*";
+ d.widthMagnitude = 1;
+ }));
+ }
+ return false;
+ }
+
+
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
+ /**
+ * @returns the resolved list of rendered child documents, displayed
+ * at their resolved pixel widths, each separated by a resizer.
+ */
+ @computed
+ private get contents(): JSX.Element[] | null {
+ const { childLayoutPairs } = this;
+ const { Document, PanelHeight } = this.props;
+ const collector: JSX.Element[] = [];
+ for (let i = 0; i < childLayoutPairs.length; i++) {
+ const { layout } = childLayoutPairs[i];
+ collector.push(
+ <div
+ className={"document-wrapper"}
+ key={Utils.GenerateGuid()}
+ >
+ <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDocument={layout.resolvedDataDoc as Doc}
+ CollectionDoc={this.props.Document}
+ PanelWidth={() => this.lookupPixels(layout)}
+ PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
+ getTransform={() => this.lookupIndividualTransform(layout)}
+ onClick={this.onChildClickHandler}
+ />
+ <WidthLabel
+ layout={layout}
+ collectionDoc={Document}
+ />
+ </div>,
+ <ResizeBar
+ width={resizerWidth}
+ key={Utils.GenerateGuid()}
+ columnUnitLength={this.getColumnUnitLength}
+ toLeft={layout}
+ toRight={childLayoutPairs[i + 1]?.layout}
+ />
+ );
+ }
+ collector.pop(); // removes the final extraneous resize bar
+ return collector;
+ }
+
+ render(): JSX.Element {
+ return (
+ <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}>
+ {this.contents}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
new file mode 100644
index 000000000..11e210958
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../../new_fields/Types";
+import { WidthUnit } from "./CollectionMulticolumnView";
+
+interface ResizerProps {
+ width: number;
+ columnUnitLength(): number | undefined;
+ toLeft?: Doc;
+ toRight?: Doc;
+}
+
+enum ResizeMode {
+ Global = "blue",
+ Pinned = "red",
+ Undefined = "black"
+}
+
+const resizerOpacity = 1;
+
+@observer
+export default class ResizeBar extends React.Component<ResizerProps> {
+ @observable private isHoverActive = false;
+ @observable private isResizingActive = false;
+ @observable private resizeMode = ResizeMode.Undefined;
+
+ @action
+ private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.resizeMode = mode;
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ window.addEventListener("pointermove", this.onPointerMove);
+ window.addEventListener("pointerup", this.onPointerUp);
+ this.isResizingActive = true;
+ }
+
+ private onPointerMove = ({ movementX }: PointerEvent) => {
+ const { toLeft, toRight, columnUnitLength } = this.props;
+ const movingRight = movementX > 0;
+ const toNarrow = movingRight ? toRight : toLeft;
+ const toWiden = movingRight ? toLeft : toRight;
+ const unitLength = columnUnitLength();
+ if (unitLength) {
+ if (toNarrow) {
+ const { widthUnit, widthMagnitude } = toNarrow;
+ const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
+ toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale;
+ }
+ if (this.resizeMode === ResizeMode.Pinned && toWiden) {
+ const { widthUnit, widthMagnitude } = toWiden;
+ const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
+ toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
+ }
+ }
+ }
+
+ private get isActivated() {
+ const { toLeft, toRight } = this.props;
+ if (toLeft && toRight) {
+ if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ } else if (toLeft) {
+ if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ } else if (toRight) {
+ if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ private onPointerUp = () => {
+ this.resizeMode = ResizeMode.Undefined;
+ this.isResizingActive = false;
+ this.isHoverActive = false;
+ window.removeEventListener("pointermove", this.onPointerMove);
+ window.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ render() {
+ return (
+ <div
+ className={"resizer"}
+ style={{
+ width: this.props.width,
+ opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
+ }}
+ onPointerEnter={action(() => this.isHoverActive = true)}
+ onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
+ >
+ <div
+ className={"internal"}
+ onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
+ style={{ backgroundColor: this.resizeMode }}
+ />
+ <div
+ className={"internal"}
+ onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
+ style={{ backgroundColor: this.resizeMode }}
+ />
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
new file mode 100644
index 000000000..b394fed62
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
+import { EditableView } from "../../EditableView";
+import { WidthUnit } from "./CollectionMulticolumnView";
+
+interface WidthLabelProps {
+ layout: Doc;
+ collectionDoc: Doc;
+ decimals?: number;
+}
+
+@observer
+export default class WidthLabel extends React.Component<WidthLabelProps> {
+
+ @computed
+ private get contents() {
+ const { layout, decimals } = this.props;
+ const getUnit = () => StrCast(layout.widthUnit);
+ const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3));
+ return (
+ <div className={"label-wrapper"}>
+ <EditableView
+ GetValue={getMagnitude}
+ SetValue={value => {
+ const converted = Number(value);
+ if (!isNaN(converted) && converted > 0) {
+ layout.widthMagnitude = converted;
+ return true;
+ }
+ return false;
+ }}
+ contents={getMagnitude()}
+ />
+ <EditableView
+ GetValue={getUnit}
+ SetValue={value => {
+ if (Object.values(WidthUnit).includes(value)) {
+ layout.widthUnit = value;
+ return true;
+ }
+ return false;
+ }}
+ contents={getUnit()}
+ />
+ </div>
+ );
+ }
+
+ render() {
+ return BoolCast(this.props.collectionDoc.showWidthLabels) ? this.contents : (null);
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index bb8a8b47b..e3bf6b5f8 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -292,7 +292,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
if (index > -1) keys.splice(index, 1);
const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
const ref = React.createRef<HTMLDivElement>();
return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
}
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 29e167ff7..325c92413 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -89,7 +89,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
async resetPan() {
if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
runInAction(() => this.canPan = false);
- if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) {
+ if (this.sourceView.props.ContainingCollectionDoc._viewType === CollectionViewType.Freeform) {
const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
@@ -165,11 +165,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
@undoBatch
openColFullScreen = (options: { context: Doc }) => {
if (LinkFollowBox.destinationDoc) {
- if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
- options.context.panX = newPanX;
- options.context.panY = newPanY;
+ if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+ options.context._panX = newPanX;
+ options.context._panY = newPanY;
}
const view = DocumentManager.Instance.getDocumentView(options.context);
view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
@@ -193,11 +193,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => {
if (LinkFollowBox.destinationDoc) {
options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
- if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
- options.context.panX = newPanX;
- options.context.panY = newPanY;
+ if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+ options.context._panX = newPanX;
+ options.context._panY = newPanY;
}
(LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
@@ -245,11 +245,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => {
if (LinkFollowBox.destinationDoc) {
options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
- if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / 2;
- options.context.panX = newPanX;
- options.context.panY = newPanY;
+ if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
+ const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
+ options.context._panX = newPanX;
+ options.context._panY = newPanY;
}
(LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
@@ -270,13 +270,13 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
const y = NumCast(LinkFollowBox.sourceDoc.y);
const x = NumCast(LinkFollowBox.sourceDoc.x);
- const width = NumCast(LinkFollowBox.sourceDoc.width);
- const height = NumCast(LinkFollowBox.sourceDoc.height);
+ const width = NumCast(LinkFollowBox.sourceDoc._width);
+ const height = NumCast(LinkFollowBox.sourceDoc._height);
alias.x = x + width + 30;
alias.y = y;
- alias.width = width;
- alias.height = height;
+ alias._width = width;
+ alias._height = height;
this.sourceView.props.addDocument(alias);
}
@@ -361,7 +361,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
get canOpenInPlace() {
if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
const colDoc = this.sourceView.props.ContainingCollectionDoc;
- if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
+ if (colDoc._viewType === CollectionViewType.Freeform) return true;
}
return false;
}
@@ -481,7 +481,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
let contextMatch: boolean = false;
if (this.selectedContextAliases) {
this.selectedContextAliases.forEach(alias => {
- if (alias.viewType === CollectionViewType.Freeform) contextMatch = true;
+ if (alias._viewType === CollectionViewType.Freeform) contextMatch = true;
});
}
if (contextMatch) return true;
@@ -523,7 +523,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
render() {
return (
- <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
+ <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document._height), width: NumCast(this.props.Document._width) }}>
<div className="linkFollowBox-header">
<div className="topHeader">
{LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
@@ -533,7 +533,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
: "" : ""}</div>
</div>
- <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}>
+ <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document._height) - 110 }}>
<div className="linkFollowBox-item">
<div className="linkFollowBox-item title">Mode</div>
<div className="linkFollowBox-itemContent">
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 52628ba4c..1a40f0c55 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -25,7 +25,7 @@ export class LinkMenu extends React.Component<Props> {
@observable private _editingLink?: Doc;
@action
- componentWillReceiveProps() {
+ componentDidMount() {
this._editingLink = undefined;
}
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index abd17ec4d..0c38ff45c 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[];
- DragManager.StartLinkTargetsDrag(this._drag.current!, e.x, e.y, this.props.sourceDoc, targets);
+ DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets);
}
e.stopPropagation();
}
@@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
if (index > -1) keys.splice(index, 1);
const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+ const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
const ref = React.createRef<HTMLDivElement>();
return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 95c765e8a..62a479b2a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -68,7 +68,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
});
this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
- const start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+ const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]);
start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
});
}
@@ -128,18 +128,17 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
recordAudioAnnotation = () => {
let gumStream: any;
const self = this;
- const extensionDoc = this.extensionDoc;
- extensionDoc && navigator.mediaDevices.getUserMedia({
+ navigator.mediaDevices.getUserMedia({
audio: true
}).then(function (stream) {
gumStream = stream;
self._recorder = new MediaRecorder(stream);
- extensionDoc.recordingStart = new DateField(new Date());
+ self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
AudioBox.ActiveRecordings.push(self.props.Document);
self._recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(Utils.prepend("/upload"), {
+ const res = await fetch(Utils.prepend("/uploadFormData"), {
method: 'POST',
body: formData
});
@@ -213,55 +212,53 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
render() {
const interactive = this.active() ? "-interactive" : "";
- return (!this.extensionDoc ? (null) :
- <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
- onClick={!this.path ? this.recordClick : undefined}>
- <div className="audiobox-handle"></div>
- {!this.path ?
- <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
- {this._audioState === "recording" ? "STOP" : "RECORD"}
- </button> :
- <div className="audiobox-controls">
- <div className="audiobox-player" onClick={this.onPlay}>
- <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
- onPointerDown={e => {
- if (e.button === 0 && !e.ctrlKey) {
- const rect = (e.target as any).getBoundingClientRect();
- this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
- this.pause();
- e.stopPropagation();
- }
- }} >
- {DocListCast(this.dataDoc.links).map((l, i) => {
- let la1 = l.anchor1 as Doc;
- let la2 = l.anchor2 as Doc;
- let linkTime = NumCast(l.anchor2Timecode);
- if (Doc.AreProtosEqual(la1, this.dataDoc)) {
- la1 = l.anchor2 as Doc;
- la2 = l.anchor1 as Doc;
- linkTime = NumCast(l.anchor1Timecode);
- }
- return !linkTime ? (null) :
- <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
- <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
- <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
- parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
- backgroundColor={returnTransparent} />
- </div>
- <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
- onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
- </div>;
- })}
- <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
- {this.audio}
- </div>
+ return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+ onClick={!this.path ? this.recordClick : undefined}>
+ <div className="audiobox-handle"></div>
+ {!this.path ?
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+ {this._audioState === "recording" ? "STOP" : "RECORD"}
+ </button> :
+ <div className="audiobox-controls">
+ <div className="audiobox-player" onClick={this.onPlay}>
+ <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
+ onPointerDown={e => {
+ if (e.button === 0 && !e.ctrlKey) {
+ const rect = (e.target as any).getBoundingClientRect();
+ this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+ this.pause();
+ e.stopPropagation();
+ }
+ }} >
+ {DocListCast(this.dataDoc.links).map((l, i) => {
+ let la1 = l.anchor1 as Doc;
+ let la2 = l.anchor2 as Doc;
+ let linkTime = NumCast(l.anchor2Timecode);
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ linkTime = NumCast(l.anchor1Timecode);
+ }
+ return !linkTime ? (null) :
+ <div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
+ <div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
+ <DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+ parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
+ backgroundColor={returnTransparent} />
+ </div>
+ <div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
+ onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+ </div>;
+ })}
+ <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+ {this.audio}
</div>
</div>
- }
- </div>
- );
+ </div>
+ }
+ </div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index d1272c266..ee48b47b7 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -36,7 +36,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
@computed get dataDoc() {
return this.props.DataDoc &&
- (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) ||
+ (this.Document.isTemplateForField || BoolCast(this.props.DataDoc.isTemplateForField) ||
this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
}
@@ -80,7 +80,10 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
return (
<div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
- <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black", fontSize: this.Document.fontSize }} >
+ <div className="buttonBox-mainButton" style={{
+ background: this.Document.backgroundColor, color: this.Document.color || "black",
+ fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: this.Document.textTransform || ""
+ }} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
</div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 261a88deb..2183129cf 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { random } from "animejs";
+import anime from "animejs";
import { computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
@@ -11,6 +11,8 @@ import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
import { PositionDocument } from "../../../new_fields/documentSchemas";
import { TraceMobx } from "../../../new_fields/util";
+import { returnFalse } from "../../../Utils";
+import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,13 +22,16 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
height?: number;
jitterRotation: number;
transition?: string;
+ fitToBox?: boolean;
}
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
_disposer: IReactionDisposer | undefined = undefined;
+
+ @observable _animPos: number[] | undefined = undefined;
get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
- get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
+ get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.random(-1, 1) * this.props.jitterRotation}deg)`; }
get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
@@ -35,57 +40,43 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
}
@computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document) ? this.props.dataProvider(this.props.Document) : undefined; }
- @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
- @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
+ @computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth); }
+ @computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight); }
@computed get renderScriptDim() {
if (this.Document.renderScript) {
const someView = Cast(this.props.Document.someView, Doc);
const minimap = Cast(this.props.Document.minimap, Doc);
if (someView instanceof Doc && minimap instanceof Doc) {
- const x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
- const y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
- const w = NumCast(someView.width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
- const h = NumCast(someView.height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
+ const x = (NumCast(someView._panX) - NumCast(someView._width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap._width) - NumCast(minimap._width) / 2;
+ const y = (NumCast(someView._panY) - NumCast(someView._height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap._height) - NumCast(minimap._height) / 2;
+ const w = NumCast(someView._width) / NumCast(someView.scale) / NumCast(minimap.fitW) * NumCast(minimap.width);
+ const h = NumCast(someView._height) / NumCast(someView.scale) / NumCast(minimap.fitH) * NumCast(minimap.height);
return { x: x, y: y, width: w, height: h };
}
}
return undefined;
}
- componentWillUnmount() {
- this._disposer && this._disposer();
- }
+ componentWillUnmount() { this._disposer?.(); }
componentDidMount() {
- this._disposer = reaction(() => this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined,
- target => this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]),
+ this._disposer = reaction(() => Array.from(Cast(this.props.Document?.animateToPos, listSpec("number"), null) || []),
+ target => this._animPos = !target || !target?.length ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] :
+ this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]),
{ fireImmediately: true });
}
- contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1;
- panelWidth = () => this.props.PanelWidth();
- panelHeight = () => this.props.PanelHeight();
+ contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1;
+ clusterColorFunc = (doc: Doc) => this.clusterColor;
+ panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth());
+ panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight());
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling())
- borderRounding = () => {
- const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
- const ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
- const br = StrCast((ld || this.props.Document).borderRounding);
- return !br && ruleRounding ? ruleRounding : br;
- }
-
@computed
get clusterColor() { return this.props.backgroundColor(this.props.Document); }
- clusterColorFunc = (doc: Doc) => this.clusterColor;
-
- @observable _animPos: number[] | undefined = undefined;
-
- finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
- finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
-
render() {
TraceMobx();
return <div className="collectionFreeFormDocumentView-container"
@@ -96,21 +87,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big
StrCast(this.layoutDoc.boxShadow, ""),
- borderRadius: this.borderRounding(),
+ borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding),
transform: this.transform,
transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
width: this.width,
height: this.height,
zIndex: this.Document.zIndex || 0,
}} >
- <DocumentView {...this.props}
+
+ {!this.props.fitToBox ? <DocumentView {...this.props}
dragDivName={"collectionFreeFormDocumentView-container"}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
backgroundColor={this.clusterColorFunc}
- PanelWidth={this.finalPanelWidth}
- PanelHeight={this.finalPanelHeight}
- />
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ /> : <ContentFittingDocumentView {...this.props}
+ CollectionDoc={this.props.ContainingCollectionDoc}
+ DataDocument={this.props.DataDoc}
+ getTransform={this.getTransform}
+ active={returnFalse}
+ focus={(doc: Doc) => this.props.focus(doc, false)}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ />}
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 2f8142a44..51c8e00da 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -24,9 +24,7 @@ interface ContentFittingDocumentViewProps {
fitToBox?: boolean;
PanelWidth: () => number;
PanelHeight: () => number;
- ruleProvider: Doc | undefined;
focus?: (doc: Doc) => void;
- showOverlays?: (doc: Doc) => { title?: string, caption?: string };
CollectionView?: CollectionView;
CollectionDoc?: Doc;
onClick?: ScriptField;
@@ -39,22 +37,20 @@ interface ContentFittingDocumentViewProps {
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
dontRegisterView?: boolean;
- setPreviewScript: (script: string) => void;
- previewScript?: string;
}
@observer
export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
- private get nativeWidth() { return NumCast(this.layoutDoc?.nativeWidth, this.props.PanelWidth()); }
- private get nativeHeight() { return NumCast(this.layoutDoc?.nativeHeight, this.props.PanelHeight()); }
+ private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); }
+ private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); }
private contentScaling = () => {
- const wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
+ const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1);
if (wscale * this.nativeHeight > this.props.PanelHeight()) {
- return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight());
+ return (this.props.PanelHeight() / (this.nativeHeight || this.props.PanelHeight() || 1)) || 1;
}
- return wscale;
+ return wscale || 1;
}
@undoBatch
@@ -65,16 +61,16 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
this.props.childDocs && this.props.childDocs.map(otherdoc => {
const target = Doc.GetProto(otherdoc);
target.layout = ComputedField.MakeFunction("this.image_data[0]");
- target.layoutCustom = Doc.MakeDelegate(docDragData.draggedDocuments[0]);
+ target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]);
});
e.stopPropagation();
}
return true;
}
- private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
- private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document.fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
+ private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
+ private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
- private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document.fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
+ private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; }
@computed get borderRounding() { return StrCast(this.props.Document?.borderRounding); }
@@ -99,8 +95,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
LibraryPath={this.props.LibraryPath}
fitToBox={this.props.fitToBox}
onClick={this.props.onClick}
- ruleProvider={this.props.ruleProvider}
- showOverlays={this.props.showOverlays}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 0d4d50c59..a4a9a62aa 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -61,10 +61,12 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
}
}
onClick = (e: React.MouseEvent) => {
- if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) {
- DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+ if (!this.props.Document.onClick) {
+ if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) {
+ DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+ }
+ e.stopPropagation();
}
- e.stopPropagation();
}
render() {
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
index 94755afec..6b7b652c6 100644
--- a/src/client/views/nodes/DocumentBox.tsx
+++ b/src/client/views/nodes/DocumentBox.tsx
@@ -96,7 +96,6 @@ export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocB
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- ruleProvider={this.props.ruleProvider}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
getTransform={this.getTransform}
@@ -106,8 +105,6 @@ export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocB
focus={this.props.focus}
active={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
- setPreviewScript={emptyFunction}
- previewScript={undefined}
/>}
</div>;
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 8f6bfc8e1..0b01e6471 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -54,14 +54,12 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
- onClick?: ScriptField,
layoutKey: string,
- hideOnLeave?: boolean
}> {
@computed get layout(): string {
TraceMobx();
if (!this.layoutDoc) return "<p>awaiting layout</p>";
- const layout = Cast(this.layoutDoc[this.props.layoutKey], "string");
+ const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string");
if (layout === undefined) {
return this.props.Document.data ?
"<FieldView {...props} fieldKey='data' />" :
@@ -77,12 +75,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") {
// if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
// then we render the layout document as a template and use this document as the data context for the template layout.
- return this.props.Document;
+ const proto = Doc.GetProto(this.props.Document);
+ return proto instanceof Promise ? undefined : proto;
}
- return this.props.DataDoc;
+ return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc;
}
get layoutDoc() {
- return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document);
+ return Doc.Layout(this.props.Document);
}
CreateBindings(): JsxBindings {
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index f44c6dd3b..2ce56c73d 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -55,7 +55,7 @@
position: absolute;
}
- .documentView-titleWrapper {
+ .documentView-titleWrapper, .documentView-titleWrapper-hover {
overflow: hidden;
color: white;
transform-origin: top left;
@@ -68,6 +68,9 @@
text-overflow: ellipsis;
white-space: pre;
}
+ .documentView-titleWrapper-hover {
+ display:none;
+ }
.documentView-searchHighlight {
position: absolute;
@@ -85,4 +88,12 @@
}
}
+}
+
+.documentView-node:hover, .documentView-node-topmost:hover {
+ > .documentView-styleWrapper {
+ > .documentView-titleWrapper-hover {
+ display:inline-block;
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fd7aad0d9..925a590ca 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -9,12 +9,12 @@ import { Id } from '../../../new_fields/FieldSymbols';
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { ImageField } from '../../../new_fields/URLField';
+import { ImageField, PdfField, VideoField, AudioField } from '../../../new_fields/URLField';
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from "../../DocServer";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DocumentManager } from "../../util/DocumentManager";
@@ -29,7 +29,6 @@ import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
-import { DictationOverlay } from '../DictationOverlay';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { OverlayView } from '../OverlayView';
@@ -49,6 +48,8 @@ import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { RadialMenu } from './RadialMenu';
import { RadialMenuProps } from './RadialMenuItem';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
+import { RichTextField } from '../../../new_fields/RichTextField';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -70,9 +71,7 @@ export interface DocumentViewProps {
moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
renderDepth: number;
- showOverlays?: (doc: Doc) => { title?: string, caption?: string };
ContentScaling: () => number;
- ruleProvider: Doc | undefined;
PanelWidth: () => number;
PanelHeight: () => number;
focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void;
@@ -110,8 +109,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active() { return SelectionManager.IsSelected(this, true) || this.props.parentActive(true); }
@computed get topMost() { return this.props.renderDepth === 0; }
- @computed get nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
- @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
+ @computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; }
+ @computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; }
@computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
@computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
@computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
@@ -155,7 +154,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
RadialMenu.Instance.openMenu();
RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView ?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Delete this document", event: () => this.props.ContainingCollectionView?.removeDocument(this.props.Document), icon: "trash", selected: -1 });
RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "folder", selected: -1 });
RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
@@ -234,13 +233,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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), 0);
+ if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0);
else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text...
{
- this._titleRef.current ?.setIsFocused(false);
- const any = (this._mainCont.current ?.getElementsByClassName("ProseMirror") ?.[0] as any);
+ this._titleRef.current?.setIsFocused(false);
+ const any = (this._mainCont.current?.getElementsByClassName("ProseMirror")?.[0] as any);
any.keeplocation = true;
- any ?.focus();
+ any?.focus();
}
}
} else if (e.key === "f") {
@@ -256,21 +255,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
e.stopPropagation();
let preventDefault = true;
- if (this._doubleTap && this.props.renderDepth && !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._doubleTap && this.props.renderDepth && !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
const fullScreenAlias = Doc.MakeAlias(this.props.Document);
- if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
- fullScreenAlias.layoutKey = "layoutCustom";
+ if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) {
+ fullScreenAlias.layoutKey = "layout_custom";
}
this.props.addDocTab(fullScreenAlias, undefined, "inTab");
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
} else if (this.onClickHandler && this.onClickHandler.script) {
- this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc }, console.log);
} else if (this.Document.type === DocumentType.BUTTON) {
ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
} else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script
FormattedTextBoxComment.Hide();
- this.Document.links ?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0]!, this.props.Document)]));
+ this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)]));
} else if (this.Document.isButton) {
SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
this.buttonClick(e.altKey, e.ctrlKey);
@@ -312,22 +311,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
if (this.Document.onPointerDown) return;
- if (!e.nativeEvent.cancelBubble) {
- const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+ const touch = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
+ console.log("down");
+ if (touch) {
this._downX = touch.clientX;
this._downY = touch.clientY;
- this._hitTemplateDrag = false;
- for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
- if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
- this._hitTemplateDrag = true;
+ if (!e.nativeEvent.cancelBubble) {
+ this._hitTemplateDrag = false;
+ for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
+ if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
+ this._hitTemplateDrag = true;
+ }
}
+ if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation();
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ e.stopPropagation();
}
- if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation();
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
}
}
@@ -341,7 +343,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
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
@@ -385,10 +387,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
const doc = PositionDocument(this.props.Document);
const layoutDoc = PositionDocument(Doc.Layout(this.props.Document));
- let nwidth = layoutDoc.nativeWidth || 0;
- let nheight = layoutDoc.nativeHeight || 0;
- const width = (layoutDoc.width || 0);
- const height = (layoutDoc.height || (nheight / nwidth * width));
+ let nwidth = layoutDoc._nativeWidth || 0;
+ let nheight = layoutDoc._nativeHeight || 0;
+ const width = (layoutDoc._width || 0);
+ const height = (layoutDoc._height || (nheight / nwidth * width));
const scale = this.props.ScreenToLocalTransform().Scale * this.props.ContentScaling();
const actualdW = Math.max(width + (dW * scale), 20);
const actualdH = Math.max(height + (dH * scale), 20);
@@ -398,34 +400,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
layoutDoc.ignoreAspect = false;
- layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
- layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+ layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+ layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
if (fixedAspect && (!nwidth || !nheight)) {
- layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
- layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
+ layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
+ layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
+ layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
}
- layoutDoc.width = actualdW;
- if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
- else layoutDoc.height = actualdH;
+ layoutDoc._width = actualdW;
+ if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
+ else layoutDoc._height = actualdH;
}
else {
if (!fixedAspect) {
- layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
+ layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
}
- layoutDoc.height = actualdH;
- if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
- else layoutDoc.width = actualdW;
+ layoutDoc._height = actualdH;
+ if (fixedAspect && !layoutDoc._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);
+ dW && (layoutDoc._width = actualdW);
+ dH && (layoutDoc._height = actualdH);
+ dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
}
}
e.stopPropagation();
@@ -435,7 +437,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerDown = (e: React.PointerEvent): void => {
if (this.onPointerDownHandler && this.onPointerDownHandler.script) {
- this.onPointerDownHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ this.onPointerDownHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
return;
@@ -482,7 +484,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
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
@@ -492,7 +494,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerUp = (e: PointerEvent): void => {
if (this.onPointerUpHandler && this.onPointerUpHandler.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- this.onPointerUpHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ this.onPointerUpHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
document.removeEventListener("pointerup", this.onPointerUp);
return;
}
@@ -514,45 +516,48 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
- static makeNativeViewClicked = (doc: Doc) => {
- undoBatch(() => doc.layoutKey = "layout")();
+ static makeNativeViewClicked = (doc: Doc, prevLayout: string) => {
+ undoBatch(() => {
+ if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, "");
+ doc.layoutKey = "layout";
+ })();
}
- static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => {
+ static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, name: string = "custom", docLayoutTemplate?: Doc) => {
const batch = UndoManager.StartBatch("CustomViewClicked");
- if (doc.layoutCustom === undefined) {
- const width = NumCast(doc.width);
- const height = NumCast(doc.height);
- const options = { title: "data", width, x: -width / 2, y: - height / 2, };
-
- let fieldTemplate: Doc;
- switch (doc.type) {
- case DocumentType.TEXT:
- fieldTemplate = Docs.Create.TextDocument(options);
- break;
- case DocumentType.PDF:
- fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
- break;
- case DocumentType.VID:
- fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
- break;
- case DocumentType.AUDIO:
- fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
- break;
- default:
- fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+ const customName = "layout_" + name;
+ if (!StrCast(doc.title).endsWith(name)) doc.title = doc.title + "_" + name;
+ if (doc[customName] === undefined) {
+ const _width = NumCast(doc._width);
+ const _height = NumCast(doc._height);
+ const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, };
+
+ const field = doc.data;
+ let fieldTemplate: Opt<Doc>;
+ if (field instanceof RichTextField || typeof (field) === "string") {
+ fieldTemplate = Docs.Create.TextDocument("", options);
+ } else if (field instanceof PdfField) {
+ fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+ } else if (field instanceof VideoField) {
+ fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+ } else if (field instanceof AudioField) {
+ fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+ } else if (field instanceof ImageField) {
+ fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
}
- fieldTemplate.backgroundColor = doc.backgroundColor;
- fieldTemplate.heading = 1;
- fieldTemplate.autoHeight = true;
+ if (fieldTemplate) {
+ fieldTemplate.backgroundColor = doc.backgroundColor;
+ fieldTemplate.heading = 1;
+ fieldTemplate._autoHeight = true;
+ }
- const docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
+ const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
- Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true);
- Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
+ fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
+ Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, customName, undefined);
} else {
- doc.layoutKey = "layoutCustom";
+ doc.layoutKey = customName;
}
batch.end();
}
@@ -591,7 +596,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
`Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`);
}
if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) {
- Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layoutCustom");
+ Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom");
e.stopPropagation();
}
if (de.complete.linkDragData) {
@@ -618,11 +623,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
freezeNativeDimensions = (): void => {
- this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false;
+ this.layoutDoc._autoHeight = false;
this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect;
- if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) {
- this.layoutDoc.nativeWidth = this.props.PanelWidth();
- this.layoutDoc.nativeHeight = this.props.PanelHeight();
+ if (!this.layoutDoc.ignoreAspect && !this.layoutDoc._nativeWidth) {
+ this.layoutDoc._nativeWidth = this.props.PanelWidth();
+ this.layoutDoc._nativeHeight = this.props.PanelHeight();
}
}
@@ -633,7 +638,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
DocServer.GetRefField(portalID).then(existingPortal => {
- const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
+ const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: portalID });
DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
this.Document.isButton = true;
});
@@ -642,13 +647,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
- setCustomView = (custom: boolean): void => {
- if (this.props.ContainingCollectionView ?.props.DataDoc || this.props.ContainingCollectionView ?.props.Document.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
- } else {
- custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
+ setCustomView =
+ (custom: boolean, layout: string): void => {
+ if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
+ } else if (custom) {
+ DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
+
+ let foundLayout: Opt<Doc> = undefined;
+ DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => {
+ if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title) === layout) {
+ foundLayout = btnDoc.dragFactory as Doc;
+ }
+ })
+ DocumentView.
+ makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout);
+ } else {
+ DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
+ }
}
- }
@undoBatch
@action
@@ -672,7 +689,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
onContextMenu = async (e: React.MouseEvent): Promise<void> => {
// the touch onContextMenu is button 0, the pointer onContextMenu is button 2
- if (e.button === 0) {
+ if (e.button === 0 && !e.ctrlKey) {
e.preventDefault();
return;
}
@@ -692,7 +709,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
+ subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
@@ -717,21 +734,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const existing = ContextMenu.Instance.findByDescription("Layout...");
const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
- if (this.props.DataDoc) {
- layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
- }
- layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
- layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
+ layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+
+ layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+ layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+ layoutItems.push({ description: this.Document.ignoreAspect || !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
- if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
- layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
- } else {
- layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" });
- }
!existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
const more = ContextMenu.Instance.findByDescription("More...");
@@ -776,13 +787,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
DocServer.setFieldWriteMode("x", mode1);
DocServer.setFieldWriteMode("y", mode1);
- DocServer.setFieldWriteMode("width", mode1);
- DocServer.setFieldWriteMode("height", mode1);
+ DocServer.setFieldWriteMode("_width", mode1);
+ DocServer.setFieldWriteMode("_height", mode1);
- DocServer.setFieldWriteMode("panX", mode2);
- DocServer.setFieldWriteMode("panY", mode2);
+ DocServer.setFieldWriteMode("_panX", mode2);
+ DocServer.setFieldWriteMode("_panY", mode2);
DocServer.setFieldWriteMode("scale", mode2);
- DocServer.setFieldWriteMode("viewType", mode2);
+ DocServer.setFieldWriteMode("_viewType", mode2);
};
const aclsMenu: ContextMenuProps[] = [];
aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
@@ -827,18 +838,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
chromeHeight = () => {
- const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
- const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- return (showTitle ? 25 : 0) + 1;
+ const showTitle = StrCast(this.layoutDoc.showTitle);
+ const showTitleHover = StrCast(this.layoutDoc.showTitleHover);
+ return (showTitle && !showTitleHover ? 0 : 0) + 1;
}
- @computed get finalLayoutKey() { return this.props.layoutKey || "layout"; }
- childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+ @computed get finalLayoutKey() {
+ const { layoutKey } = this.props;
+ if (typeof layoutKey === "string") {
+ return layoutKey;
+ }
+ const fallback = Cast(this.props.Document.layoutKey, "string");
+ return typeof fallback === "string" ? fallback : "layout";
+ }
+ childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
@computed get contents() {
TraceMobx();
return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
Document={this.props.Document}
+ DataDoc={this.props.DataDoc}
fitToBox={this.props.fitToBox}
LibraryPath={this.props.LibraryPath}
addDocument={this.props.addDocument}
@@ -846,9 +865,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moveDocument={this.props.moveDocument}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth}
- showOverlays={this.props.showOverlays}
ContentScaling={this.childScaling}
- ruleProvider={this.props.ruleProvider}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
focus={this.props.focus}
@@ -865,8 +882,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
isSelected={this.isSelected}
select={this.select}
onClick={this.onClickHandler}
- layoutKey={this.finalLayoutKey}
- DataDoc={this.props.DataDoc} />);
+ layoutKey={this.finalLayoutKey} />);
}
linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
@@ -881,9 +897,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get innards() {
TraceMobx();
- const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
- const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.getLayoutPropStr("showTitle"));
- const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
+ const showTitle = StrCast(this.getLayoutPropStr("showTitle"));
+ const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover"));
+ const showCaption = this.getLayoutPropStr("showCaption");
const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
const searchHighlight = (!this.Document.searchFields ? (null) :
<div className="documentView-searchHighlight">
@@ -898,15 +914,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
/>
</div>);
const titleView = (!showTitle ? (null) :
- <div className="documentView-titleWrapper" style={{
+ <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{
position: showTextTitle ? "relative" : "absolute",
pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
}}>
<EditableView ref={this._titleRef}
- contents={this.Document[showTitle]}
+ contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()}
display={"block"} height={72} fontSize={12}
- GetValue={() => StrCast(this.Document[showTitle])}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true)}
+ GetValue={() => (this.props.DataDoc || this.props.Document)[showTitle]?.toString()}
+ SetValue={undoBatch((value: string) => (Doc.GetProto(this.props.DataDoc || this.props.Document)[showTitle] = value) ? true : true)}
/>
</div>);
return <>
@@ -940,16 +956,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
render() {
if (!(this.props.Document instanceof Doc)) return (null);
- const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
- const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
const colorSet = this.setsLayoutProp("backgroundColor");
const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
const backgroundColor = (clusterCol && !colorSet) ?
this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
- ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
+ StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
- const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding;
+ const borderRounding = this.getLayoutPropStr("borderRounding");
const localScale = fullDegree;
const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
@@ -958,7 +972,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
- let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
+ let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
@@ -969,7 +983,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
color: StrCast(this.Document.color),
outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
- background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
+ boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined,
+ background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor,
width: animwidth,
height: animheight,
opacity: this.Document.opacity
@@ -979,4 +994,4 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; }); \ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layout_custom" : "layout"; }); \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c56fde186..dbbb76f83 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -27,7 +27,6 @@ export interface FieldViewProps {
fitToBox?: boolean;
ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
- ruleProvider: Doc | undefined;
Document: Doc;
DataDoc?: Doc;
LibraryPath: Doc[];
@@ -54,7 +53,7 @@ export interface FieldViewProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string) {
- return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />"
+ return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
}
@computed
@@ -70,12 +69,12 @@ export class FieldView extends React.Component<FieldViewProps> {
// if (typeof field === "string") {
// return <p>{field}</p>;
// }
- else if (field instanceof RichTextField) {
- return <FormattedTextBox {...this.props} />;
- }
- else if (field instanceof ImageField) {
- return <ImageBox {...this.props} />;
- }
+ // else if (field instanceof RichTextField) {
+ // return <FormattedTextBox {...this.props} />;
+ // }
+ // else if (field instanceof ImageField) {
+ // return <ImageBox {...this.props} />;
+ // }
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
// }
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 2433251b3..a191ac4f4 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -5,10 +5,11 @@ import { createSchema, makeInterface } from '../../../new_fields/Schema';
import { DocComponent } from '../DocComponent';
import './FontIconBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
-import { StrCast } from '../../../new_fields/Types';
+import { StrCast, Cast } from '../../../new_fields/Types';
import { Utils } from "../../../Utils";
import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
import { Doc } from '../../../new_fields/Doc';
+import { ContextMenu } from '../ContextMenu';
const FontIconSchema = createSchema({
icon: "string"
});
@@ -32,13 +33,25 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
}
}, { fireImmediately: true });
}
+
+ showTemplate = (): void => {
+ const dragFactory = Cast(this.props.Document.dragFactory, Doc, null);
+ dragFactory && this.props.addDocTab(dragFactory, undefined, "onRight");
+ }
+
+ specificContextMenu = (): void => {
+ const cm = ContextMenu.Instance;
+ cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" });
+ }
+
componentWillUnmount() {
- this._backgroundReaction && this._backgroundReaction();
+ this._backgroundReaction?.();
}
+
render() {
const referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
const referenceLayout = Doc.Layout(referenceDoc);
- return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
+ return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref} onContextMenu={this.specificContextMenu}
style={{
background: StrCast(referenceLayout.backgroundColor),
boxShadow: this.props.Document.ischecked ? `4px 4px 12px black` : undefined
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 7555a594b..0d97c3029 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,7 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace, _allowStateChangesInsideComputed } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
@@ -12,7 +12,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym, DataSym, Field } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { RichTextField } from "../../../new_fields/RichTextField";
import { RichTextUtils } from '../../../new_fields/RichTextUtils';
@@ -27,12 +27,10 @@ import { DictationManager } from '../../util/DictationManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema";
+import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView, DashFieldView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
-import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
-import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocAnnotatableComponent } from "../DocComponent";
+import { DocAnnotatableComponent, DocAnnotatableProps } from "../DocComponent";
import { DocumentButtonBar } from '../DocumentButtonBar';
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
@@ -47,16 +45,13 @@ import { AudioBox } from './AudioBox';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { InkTool } from '../../../new_fields/InkField';
import { TraceMobx } from '../../../new_fields/util';
+import RichTextMenu from '../../util/RichTextMenu';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
hideOnLeave?: boolean;
- height?: string;
- color?: string;
- outer_div?: (domminus: HTMLElement) => void;
- firstinstance?: boolean;
}
const richTextSchema = createSchema({
@@ -75,7 +70,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
- public static ToolTipTextMenu: TooltipTextMenu | undefined = undefined;
public ProseRef?: HTMLDivElement;
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -90,16 +84,12 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
private _reactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
- private _rulesReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
private _pullReactionDisposer: Opt<IReactionDisposer>;
private _pushReactionDisposer: Opt<IReactionDisposer>;
private _buttonBarReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- @observable private _ruleFontSize = 0;
- @observable private _ruleFontFamily = "Arial";
- @observable private _fontAlign = "";
@observable private _entered = false;
public static FocusedBox: FormattedTextBox | undefined;
@@ -125,10 +115,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
return "";
}
- public static getToolTip(ev: EditorView) {
- return this.ToolTipTextMenu ? this.ToolTipTextMenu : this.ToolTipTextMenu = new TooltipTextMenu(ev);
- }
-
@undoBatch
public setFontColor(color: string) {
const view = this._editorView!;
@@ -158,7 +144,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
DocServer.GetRefField(value).then(doc => {
DocServer.GetRefField(id).then(linkDoc => {
- this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+ this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
@@ -199,9 +185,11 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
const tsel = this._editorView.state.selection.$from;
tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
this._applyingChange = true;
- this.extensionDoc && !this.extensionDoc.lastModified && (this.extensionDoc.backgroundColor = "lightGray");
- this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ if (!this.props.Document._textTemplate || Doc.GetProto(this.props.Document) === this.dataDoc) {
+ this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray");
+ this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ }
this._applyingChange = false;
this.updateTitle();
this.tryUpdateHeight();
@@ -269,15 +257,15 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
newLayout = Doc.MakeDelegate(draggedDoc);
newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`);
}
- this.Document.layoutCustom = newLayout;
- this.Document.layoutKey = "layoutCustom";
+ this.Document.layout_custom = newLayout;
+ this.Document.layoutKey = "layout_custom";
e.stopPropagation();
// embed document when dragging with a userDropAction or an embedDoc flag set
} else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) {
const target = de.complete.docDragData.droppedDocuments[0];
// const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
// if (link) {
- target.fitToBox = true;
+ target._fitToBox = true;
const node = schema.nodes.dashDoc.create({
width: target[WidthSym](), height: target[HeightSym](),
title: "dashDoc", docid: target[Id],
@@ -483,11 +471,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
schema,
plugins: [
inputRules(inpRules),
- this.tooltipTextMenuPlugin(),
+ this.richTextMenuPlugin(),
history(),
keymap(this._keymap),
keymap(baseKeymap),
- // this.tooltipLinkingMenuPlugin(),
new Plugin({
props: {
attributes: { class: "ProseMirror-example-setup-style" }
@@ -511,7 +498,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this._reactionDisposer = reaction(
() => {
- const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
+ const field = Cast(this.props.Document._textTemplate || this.dataDoc[this.props.fieldKey], RichTextField);
return field ? field.Data : RichTextUtils.Initialize();
},
incomingValue => {
@@ -545,46 +532,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
);
this._heightReactionDisposer = reaction(
- () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight],
+ () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight],
() => this.tryUpdateHeight()
);
- this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
+ this.setupEditor(this.config, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => this.layoutDoc.searchMatch,
search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
{ fireImmediately: true });
- this._rulesReactionDisposer = reaction(() => {
- const ruleProvider = this.props.ruleProvider;
- const heading = NumCast(this.layoutDoc.heading);
- if (ruleProvider instanceof Doc) {
- return {
- align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
- font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"),
- size: NumCast(ruleProvider["ruleSize_" + heading], 13)
- };
- }
- return undefined;
- },
- action((rules: any) => {
- this._ruleFontFamily = rules ? rules.font : "Arial";
- this._ruleFontSize = rules ? rules.size : 0;
- rules && setTimeout(() => {
- const view = this._editorView!;
- if (this.ProseRef) {
- const n = new NodeSelection(view.state.doc.resolve(0));
- if (this._editorView!.state.doc.textContent === "") {
- view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))).
- replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true));
- } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) {
- view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align }));
- }
- this.tryUpdateHeight();
- }
- }, 0);
- }), { fireImmediately: true }
- );
this._scrollToRegionReactionDisposer = reaction(
() => StrCast(this.layoutDoc.scrollToLinkID),
async (scrollToLinkID) => {
@@ -741,11 +698,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
setTimeout(async () => {
- const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
- if (extension) {
- const targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations
- targetAnnotations && targetAnnotations.push(pdfRegion);
- }
+ const targetField = Doc.LayoutFieldKey(pdfDoc);
+ const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations
+ targetAnnotations?.push(pdfRegion);
});
const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
@@ -783,28 +738,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
}
- private setupEditor(config: any, doc: Doc, fieldKey: string) {
- const field = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
- let startup = StrCast(doc.documentText);
- startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : "";
- if (!field && doc) {
- const text = StrCast(doc[fieldKey]);
- if (text) {
- startup = text;
- } else if (Cast(doc[fieldKey], "number")) {
- startup = NumCast(doc[fieldKey], 99).toString();
- }
- }
+ private setupEditor(config: any, fieldKey: string) {
+ const rtfField = Cast(this.props.Document._textTemplate || this.dataDoc[fieldKey], RichTextField);
if (this.ProseRef) {
const self = this;
- this._editorView && this._editorView.destroy();
+ this._editorView?.destroy();
this._editorView = new EditorView(this.ProseRef, {
- state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
+ state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
handleScrollToSelection: (editorView) => {
const ref = editorView.domAtPos(editorView.state.selection.from);
let refNode = ref.node as any;
while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement;
- const r1 = refNode && refNode.getBoundingClientRect();
+ const r1 = refNode?.getBoundingClientRect();
const r3 = self._ref.current!.getBoundingClientRect();
if (r1.top < r3.top || r1.top > r3.bottom) {
r1 && (self._scrollRef.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale);
@@ -814,6 +759,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); },
+ dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); },
dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
summary(node, view, getPos) { return new SummaryView(node, view, getPos); },
@@ -824,9 +770,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
handlePaste: this.handlePaste,
});
this._editorView.state.schema.Document = this.props.Document;
- if (startup && this._editorView) {
- Doc.GetProto(doc).documentText = undefined;
- this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
+ const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
+ if (startupText) {
+ this._editorView.dispatch(this._editorView.state.tr.insertText(startupText));
}
}
@@ -835,8 +781,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
FormattedTextBox.SelectOnLoad = "";
this.props.select(false);
}
- const rtf = doc ? Cast(doc[fieldKey], RichTextField) : undefined;
- (selectOnLoad || (rtf && !rtf.Text)) && this._editorView!.focus();
+ (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
}
@@ -855,7 +800,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
componentWillUnmount() {
this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer();
- this._rulesReactionDisposer && this._rulesReactionDisposer();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
this._pushReactionDisposer && this._pushReactionDisposer();
@@ -909,6 +853,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
prosediv && (prosediv.keeplocation = undefined);
const pos = this._editorView?.state.selection.$from.pos || 1;
keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+
+ // jump rich text menu to this textbox
+ const { current } = this._ref;
+ if (current) {
+ const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
+ const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
+ RichTextMenu.Instance.jumpTo(x, y);
+ }
}
onPointerWheel = (e: React.WheelEvent): void => {
// if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -930,7 +882,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
if (!node && this.ProseRef) {
const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div
- if (e.clientY > lastNode.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
+ if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
}
}
@@ -1028,23 +980,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
}
- tooltipTextMenuPlugin() {
+ richTextMenuPlugin() {
const self = FormattedTextBox;
return new Plugin({
view(newView) {
- return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView);
+ RichTextMenu.Instance.changeView(newView);
+ return RichTextMenu.Instance;
}
});
}
- tooltipLinkingMenuPlugin() {
- const myprops = this.props;
- return new Plugin({
- view(_editorView) {
- return new TooltipLinkingMenu(_editorView, myprops);
- }
- });
- }
onBlur = (e: any) => {
//DictationManager.Controls.stop(false);
if (this._undoTyping) {
@@ -1052,6 +997,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this._undoTyping = undefined;
}
this.doLinkOnDeselect();
+
+ // move the richtextmenu offscreen
+ if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
}
_lastTimedMark: Mark | undefined = undefined;
@@ -1096,45 +1044,46 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
@action
tryUpdateHeight(limitHeight?: number) {
let scrollHeight = this._ref.current?.scrollHeight;
- if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight &&
+ if (!this.layoutDoc.animateToPos && this.layoutDoc._autoHeight && scrollHeight &&
getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
if (limitHeight && scrollHeight > limitHeight) {
scrollHeight = limitHeight;
this.layoutDoc.limitHeight = undefined;
- this.layoutDoc.autoHeight = false;
+ this.layoutDoc._autoHeight = false;
}
- const nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
- const dh = NumCast(this.layoutDoc.height, 0);
+ const nh = this.Document.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
+ const dh = NumCast(this.layoutDoc._height, 0);
const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle
- this.layoutDoc.height = newHeight;
- this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
+ this.layoutDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
}
}
}
@computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
@computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); }
- @computed get annotationsKey() { return "annotations"; }
+ @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
render() {
TraceMobx();
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
if (this.props.isSelected()) {
- FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+ // TODO: ftong --> update from dash in richtextmenu
+ RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props);
} else if (FormattedTextBoxComment.textBox === this) {
FormattedTextBoxComment.Hide();
}
return (
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
+ height: this.layoutDoc._autoHeight ? "max-content" : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
- color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
+ color: this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive ? "none" : "all",
- fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13),
- fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
+ fontSize: NumCast(this.layoutDoc.fontSize, 13),
+ fontFamily: StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
@@ -1152,14 +1101,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
<div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} ref={this._scrollRef}>
<div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
</div>
- {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
+ {this.props.Document._hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
<div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}>
+ style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={() => this.sidebarWidth}
- annotationsKey={this.annotationsKey}
+ annotationsKey={this.annotationKey}
isAnnotationOverlay={false}
focus={this.props.focus}
isSelected={this.props.isSelected}
@@ -1169,10 +1118,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
whenActiveChanged={this.whenActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={(doc: Doc) => { doc.hideSidebar = true; return this.addDocument(doc); }}
+ addDocument={(doc: Doc) => { doc._hideSidebar = true; return this.addDocument(doc); }}
CollectionView={undefined}
ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)}
- ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
chromeCollapsed={true}>
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 5fd5d4ce1..fda3e3285 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -86,7 +86,7 @@ export class FormattedTextBoxComment {
DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
(doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"));
} else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
- textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, width: 200, height: 400 }), undefined, "onRight");
+ textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), undefined, "onRight");
}
keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
@@ -183,10 +183,8 @@ export class FormattedTextBoxComment {
moveDocument={returnFalse}
getTransform={Transform.Identity}
active={returnFalse}
- setPreviewScript={returnEmptyString}
addDocument={returnFalse}
removeDocument={returnFalse}
- ruleProvider={undefined}
addDocTab={returnFalse}
pinToPres={returnFalse}
dontRegisterView={true}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 9462ff024..172338eb6 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -79,7 +79,7 @@ export class IconBox extends React.Component<FieldViewProps> {
<Measure offset onResize={(r) => runInAction(() => {
if (r.offset!.width || this.props.Document.hideLabel) {
this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
- if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.iconWidth;
+ if (this.props.Document._height === Number(MINIMIZED_ICON_SIZE)) this.props.Document._width = this.props.Document.iconWidth;
}
})}>
{({ measureRef }) =>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index cf5d999a7..43f4a0ba9 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,10 +1,12 @@
-.imageBox, .imageBox-dragging{
+.imageBox,
+.imageBox-dragging {
pointer-events: all;
border-radius: inherit;
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
position: absolute;
transform-origin: top left;
+
.imageBox-fader {
pointer-events: all;
}
@@ -16,6 +18,14 @@
}
}
+#upload-icon {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 20px;
+ height: 20px;
+}
+
.imageBox-cont {
padding: 0vw;
position: absolute;
@@ -25,7 +35,8 @@
max-width: 100%;
max-height: 100%;
pointer-events: none;
- background:transparent;
+ background: transparent;
+
img {
height: auto;
width: 100%;
@@ -55,9 +66,10 @@
padding: 3px;
background: white;
cursor: pointer;
- opacity:0.15;
+ opacity: 0.15;
}
-#google-photos:hover{
+
+#google-photos:hover {
opacity: 1;
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 09e627078..33c694c6e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -4,11 +4,11 @@ import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/fre
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
import { ComputedField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from '../../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { AudioField, ImageField } from '../../../new_fields/URLField';
import { Utils, returnOne, emptyFunction } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
@@ -24,9 +24,12 @@ import "./ImageBox.scss";
import React = require("react");
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id } from '../../../new_fields/FieldSymbols';
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
import { TraceMobx } from '../../../new_fields/util';
import { SelectionManager } from '../../util/SelectionManager';
+import { cache } from 'sharp';
+import { ObjectField } from '../../../new_fields/ObjectField';
+import { Networking } from '../../Network';
const requestImageSize = require('../../util/request-image-size');
const path = require('path');
const { Howl } = require('howler');
@@ -39,7 +42,6 @@ library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
curPage: "number",
fitWidth: "boolean",
- rotation: "number",
googlePhotosUrl: "string",
googlePhotosTags: "string"
});
@@ -56,6 +58,13 @@ declare class MediaRecorder {
type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
const ImageDocument = makeInterface(pageSchema, documentSchema);
+const uploadIcons = {
+ idle: "downarrow.png",
+ loading: "loading.gif",
+ success: "greencheck.png",
+ failure: "redx.png"
+};
+
@observer
export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
@@ -63,6 +72,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
private _dropDisposer?: DragManager.DragDropDisposer;
@observable private _audioState = 0;
@observable static _showControls: boolean;
+ @observable uploadIcon = uploadIcons.idle;
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer && this._dropDisposer();
@@ -73,14 +83,21 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- if (de.altKey && de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0].data instanceof ImageField) {
- Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.complete.docDragData.draggedDocuments[0].data.url);
- e.stopPropagation();
+ if (de.metaKey) {
+ de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
+ Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop);
+ e.stopPropagation();
+ }));
+ } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) {
+ const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
+ const targetField = Doc.LayoutFieldKey(layoutDoc);
+ if (layoutDoc?.[DataSym][targetField] instanceof ImageField) {
+ this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField);
+ this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]);
+ this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]);
+ e.stopPropagation();
+ }
}
- de.metaKey && de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
- this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
- e.stopPropagation();
- }));
}
}
@@ -88,8 +105,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
let gumStream: any;
let recorder: any;
const self = this;
- const extensionDoc = this.extensionDoc;
- extensionDoc && navigator.mediaDevices.getUserMedia({
+ navigator.mediaDevices.getUserMedia({
audio: true
}).then(function (stream) {
gumStream = stream;
@@ -97,18 +113,18 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(Utils.prepend("/upload"), {
+ const res = await fetch(Utils.prepend("/uploadFormData"), {
method: 'POST',
body: formData
});
const files = await res.json();
const url = Utils.prepend(files[0].path);
// upload to server with known URL
- const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
+ const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 });
audioDoc.treeViewExpandedView = "layout";
- const audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
+ const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc));
if (audioAnnos === undefined) {
- extensionDoc.audioAnnotations = new List([audioDoc]);
+ this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
} else {
audioAnnos.push(audioDoc);
}
@@ -125,15 +141,15 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@undoBatch
rotate = action(() => {
- const nw = this.Document.nativeWidth;
- const nh = this.Document.nativeHeight;
- const w = this.Document.width;
- const h = this.Document.height;
- this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360;
- this.Document.nativeWidth = nh;
- this.Document.nativeHeight = nw;
- this.Document.width = h;
- this.Document.height = w;
+ const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]);
+ const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]);
+ const w = this.Document._width;
+ const h = this.Document._height;
+ this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360;
+ this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh;
+ this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw;
+ this.Document._width = h;
+ this.Document._height = w;
});
specificContextMenu = (e: React.MouseEvent): void => {
@@ -159,7 +175,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
return faceDocs;
};
- this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter);
}
generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -171,12 +187,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
const sanitized = tag.name.replace(" ", "_");
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
- this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
+ this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList;
tagDoc.title = "Generated Tags Doc";
tagDoc.confidence = threshold;
return tagDoc;
};
- this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
}
@computed private get url() {
@@ -206,53 +222,52 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
if (this._curSuffix === "_m") this._mediumRetryCount++;
if (this._curSuffix === "_l") this._largeRetryCount++;
}
- @action onError = () => {
+ @action onError = (error: any) => {
const timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
if (timeout < 10) {
- setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ // setTimeout(this.retryPath, 500);
+ }
+ const original = StrCast(this.dataDoc.originalUrl);
+ if (error.type === "error" && original) {
+ this.dataDoc[this.props.fieldKey] = new ImageField(original);
}
}
_curSuffix = "_m";
- _resized = false;
- resize = (srcpath: string) => {
- requestImageSize(srcpath)
- .then((size: any) => {
- const rotation = NumCast(this.dataDoc.rotation) % 180;
- const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
- const aspect = realsize.height / realsize.width;
- if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
- setTimeout(action(() => {
- this._resized = true;
- this.Document.height = this.Document[WidthSym]() * aspect;
- this.Document.nativeHeight = realsize.height;
- this.Document.nativeWidth = realsize.width;
- }), 0);
- } else this._resized = true;
- })
- .catch((err: any) => console.log(err));
- }
- fadesize = (srcpath: string) => {
- requestImageSize(srcpath)
- .then((size: any) => {
- const rotation = NumCast(this.dataDoc.rotation) % 180;
- const realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
- const aspect = realsize.height / realsize.width;
- if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
- setTimeout(action(() => {
- this.Document.height = this.Document[WidthSym]() * aspect;
- this.Document.nativeHeight = realsize.height;
- this.Document.nativeWidth = realsize.width;
- }), 0);
- }
+ resize = (imgPath: string) => {
+ const cachedNativeSize = {
+ width: NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]),
+ height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"])
+ };
+ const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"];
+ if (!cachedNativeSize.width || !cachedNativeSize.height || imgPath !== cachedImgPath) {
+ (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => {
+ const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
+ const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
+ const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
+ const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
+ setTimeout(action(() => {
+ if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
+ this.Document._height = this.Document[WidthSym]() * rotatedAspect;
+ this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
+ this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
+ }
+ this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath;
+ }), 0);
})
- .catch((err: any) => console.log(err));
+ .catch((err: any) => console.log(err));
+ } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) {
+ !(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => {
+ this.Document._nativeWidth = cachedNativeSize.width;
+ this.Document._nativeHeight = cachedNativeSize.height;
+ }, 0);
+ }
}
@action
onPointerEnter = () => {
const self = this;
- const audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+ const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]);
if (audioAnnos && audioAnnos.length && this._audioState === 0) {
const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
anno.data instanceof AudioField && new Howl({
@@ -285,38 +300,78 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
}
- @computed get content() {
- TraceMobx();
- const extensionDoc = this.extensionDoc;
- if (!extensionDoc) return (null);
- // let transform = this.props.ScreenToLocalTransform().inverse();
+ @computed
+ private get considerDownloadIcon() {
+ const data = this.dataDoc[this.props.fieldKey];
+ if (!(data instanceof ImageField)) {
+ return (null);
+ }
+ const primary = data.url.href;
+ if (primary.includes(window.location.origin)) {
+ return (null);
+ }
+ return (
+ <img
+ id={"upload-icon"}
+ src={`/assets/${this.uploadIcon}`}
+ onClick={async () => {
+ const { dataDoc } = this;
+ const { success, failure, idle, loading } = uploadIcons;
+ runInAction(() => this.uploadIcon = loading);
+ const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+ dataDoc.originalUrl = primary;
+ let succeeded = true;
+ let data: ImageField | undefined;
+ try {
+ data = new ImageField(Utils.prepend(clientAccessPath));
+ } catch {
+ succeeded = false;
+ }
+ runInAction(() => this.uploadIcon = succeeded ? success : failure);
+ setTimeout(action(() => {
+ this.uploadIcon = idle;
+ if (data) {
+ dataDoc[this.props.fieldKey] = data;
+ }
+ }), 2000);
+ }}
+ />
+ );
+ }
+
+ @computed get nativeSize() {
const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
- // var [sptX, sptY] = transform.transformPoint(0, 0);
- // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
- // let w = bptX - sptX;
+ const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
+ const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1);
+ return { nativeWidth, nativeHeight };
+ }
- const nativeWidth = (this.Document.nativeWidth || pw);
- const nativeHeight = (this.Document.nativeHeight || 1);
- let paths = [[Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png"), nativeWidth / nativeHeight]];
+ @computed get paths() {
+ let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
// this._curSuffix = "";
// if (w > 20) {
- const alts = DocListCast(extensionDoc.Alternates);
- const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => [this.choosePath((doc.data as ImageField).url), doc[WidthSym]() / doc[HeightSym]()]);
+ const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]);
+ const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
const field = this.dataDoc[this.props.fieldKey];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
// else if (this._largeRetryCount < 10) this._curSuffix = "_l";
- if (field instanceof ImageField) paths = [[this.choosePath(field.url), nativeWidth / nativeHeight]];
+ if (field instanceof ImageField) paths = [this.choosePath(field.url)];
paths.push(...altpaths);
- // }
- const rotation = NumCast(this.Document.rotation, 0);
+ return paths;
+ }
+
+ @computed get content() {
+ TraceMobx();
+
+ const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)];
+ const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
+ const { nativeWidth, nativeHeight } = this.nativeSize;
+ const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]);
const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
- const srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][0] as string;
- const srcaspect = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))][1] as number;
- const fadepath = paths[Math.min(paths.length - 1, 1)][0] as string;
- !this.Document.ignoreAspect && !this._resized && this.resize(srcpath);
+ !this.Document.ignoreAspect && this.resize(srcpath);
return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<div className="imageBox-fader" >
@@ -326,7 +381,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
width={nativeWidth}
ref={this._imgRef}
onError={this.onError} />
- {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker" style={{ width: nativeWidth, height: nativeWidth / srcaspect }}>
+ {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker">
<img className="imageBox-fadeaway"
key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={fadepath}
@@ -341,10 +396,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
>
<FontAwesomeIcon className="imageBox-audioFont"
- style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" />
+ style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
+ icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
</div>
+ {this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
- <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
+ <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>;
}
@@ -361,7 +418,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.props.PanelWidth}
- annotationsKey={this.annotationsKey}
+ annotationsKey={this.annotationKey}
isAnnotationOverlay={true}
focus={this.props.focus}
isSelected={this.props.isSelected}
@@ -374,7 +431,6 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
addDocument={this.addDocument}
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
chromeCollapsed={true}>
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 234a6a9d3..7aad6f90e 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -179,7 +179,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
getTemplate = async () => {
- const parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" });
+ const parent = Docs.Create.StackingDocument([], { _width: 800, _height: 800, title: "Template" });
parent.singleColumn = false;
parent.columnWidth = 100;
for (const row of this.rows.filter(row => row.isChecked)) {
@@ -200,17 +200,17 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
if (!fieldTemplate) {
return;
}
- const previousViewType = fieldTemplate.viewType;
+ const previousViewType = fieldTemplate._viewType;
Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
- previousViewType && (fieldTemplate.viewType = previousViewType);
+ previousViewType && (fieldTemplate._viewType = previousViewType);
Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
}
inferType = async (data: FieldResult, metaKey: string) => {
- const options = { width: 300, height: 300, title: metaKey };
+ const options = { _width: 300, _height: 300, title: metaKey };
if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") {
- return Docs.Create.TextDocument(options);
+ return Docs.Create.TextDocument("", options);
} else if (data instanceof List) {
if (data.length === 0) {
return Docs.Create.StackingDocument([], options);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 91f8bb3b0..e6b512adf 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -46,7 +46,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 }), undefined, "onRight"), icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
}
@@ -58,7 +58,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
LibraryPath: [],
ContainingCollectionView: undefined,
ContainingCollectionDoc: undefined,
- ruleProvider: undefined,
fieldKey: this.props.keyName,
isSelected: returnFalse,
select: emptyFunction,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8370df6ba..e1c5fd27f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -50,9 +50,9 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
constructor(props: any) {
super(props);
this._initialScale = this.props.ScreenToLocalTransform().Scale;
- const nw = this.Document.nativeWidth = NumCast(this.extensionDocSync.nativeWidth, NumCast(this.Document.nativeWidth, 927));
- const nh = this.Document.nativeHeight = NumCast(this.extensionDocSync.nativeHeight, NumCast(this.Document.nativeHeight, 1200));
- !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
+ const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927));
+ const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200));
+ !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
const backup = "oldPath";
const { Document } = this.props;
@@ -90,10 +90,10 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
}
loaded = (nw: number, nh: number, np: number) => {
- this.extensionDocSync.numPages = np;
- this.extensionDocSync.nativeWidth = this.Document.nativeWidth = nw * 96 / 72;
- this.extensionDocSync.nativeHeight = this.Document.nativeHeight = nh * 96 / 72;
- !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
+ this.dataDoc[this.props.fieldKey + "-numPages"] = np;
+ this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72;
+ this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72;
+ !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
}
public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
@@ -211,7 +211,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
const funcs: ContextMenuProps[] = [];
pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
- funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Fit Width " + (this.Document._fitWidth ? "Off" : "On"), event: () => this.Document._fitWidth = !this.Document._fitWidth, icon: "expand-arrows-alt" });
ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
@@ -220,8 +220,8 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
@computed get renderTitleBox() {
const classname = "pdfBox" + (this.active() ? "-interactive" : "");
return <div className={classname} style={{
- width: !this.props.Document.fitWidth ? this.Document.nativeWidth || 0 : `${100 / this.contentScaling}%`,
- height: !this.props.Document.fitWidth ? this.Document.nativeHeight || 0 : `${100 / this.contentScaling}%`,
+ width: !this.props.Document._fitWidth ? this.Document._nativeWidth || 0 : `${100 / this.contentScaling}%`,
+ height: !this.props.Document._fitWidth ? this.Document._nativeHeight || 0 : `${100 / this.contentScaling}%`,
transform: `scale(${this.contentScaling})`
}} >
<div className="pdfBox-title-outer">
@@ -252,13 +252,15 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
- if (pdfUrl && this.extensionDoc && (this._everActive || (this.extensionDoc.nativeWidth && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+ if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
if (pdfUrl instanceof PdfField && this._pdf) {
return this.renderPdfView;
}
if (!this._pdfjsRequested) {
this._pdfjsRequested = true;
- Pdfjs.getDocument(pdfUrl.url.href).promise.then(pdf => runInAction(() => this._pdf = pdf));
+ const promise = Pdfjs.getDocument(pdfUrl.url.href).promise;
+ promise.then(pdf => { runInAction(() => { this._pdf = pdf; console.log("promise"); }) });
+
}
}
return this.renderTitleBox;
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 1e6894f37..428e9aa7b 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -44,7 +44,7 @@ export class PresBox extends React.Component<FieldViewProps> {
if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
Doc.GetProto(pinDoc).presentationTargetDoc = item;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
+ Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('this.presentationTargetDoc?.title?.toString()');
value.splice(i, 1, pinDoc);
}
});
@@ -336,13 +336,13 @@ export class PresBox extends React.Component<FieldViewProps> {
*/
@action
initializeScaleViews = (docList: Doc[], viewtype: number) => {
- this.props.Document.chromeStatus = "disabled";
+ this.props.Document._chromeStatus = "disabled";
const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72;
docList.forEach((doc: Doc) => {
doc.presBox = this.props.Document;
doc.presBoxKey = this.props.fieldKey;
doc.collapsedHeight = hgt;
- doc.height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)");
+ doc._height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)");
const curScale = NumCast(doc.viewScale, null);
if (curScale === undefined) {
doc.viewScale = 1;
@@ -360,7 +360,7 @@ export class PresBox extends React.Component<FieldViewProps> {
return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight
}
render() {
- this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType));
+ this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType));
return (
<div className="presBox-cont" onContextMenu={this.specificContextMenu}>
<div className="presBox-buttons">
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 376d27380..d12a8d151 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -55,12 +55,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
videoLoad = () => {
const aspect = this.player!.videoWidth / this.player!.videoHeight;
- const nativeWidth = (this.Document.nativeWidth || 0);
- const nativeHeight = (this.Document.nativeHeight || 0);
+ const nativeWidth = (this.Document._nativeWidth || 0);
+ const nativeHeight = (this.Document._nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
- if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
- this.Document.nativeHeight = (this.Document.nativeWidth || 0) / aspect;
- this.Document.height = (this.Document.width || 0) / aspect;
+ if (!this.Document._nativeWidth) this.Document._nativeWidth = this.player!.videoWidth;
+ this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect;
+ this.Document._height = (this.Document._width || 0) / aspect;
}
if (!this.Document.duration) this.Document.duration = this.player!.duration;
}
@@ -101,11 +101,11 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
}
@action public Snapshot() {
- const width = this.Document.width || 0;
- const height = this.Document.height || 0;
+ const width = this.Document._width || 0;
+ const height = this.Document._height || 0;
const canvas = document.createElement('canvas');
canvas.width = 640;
- canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
+ canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1);
const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
if (ctx) {
ctx.rect(0, 0, canvas.width, canvas.height);
@@ -117,7 +117,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
if (!this._videoRef) { // can't find a way to take snapshots of videos
const b = Docs.Create.ButtonDocument({
x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
- width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString()
+ _width: 150, _height: 50, title: (this.Document.currentTimecode || 0).toString()
});
b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`);
} else {
@@ -130,7 +130,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
const url = this.choosePath(Utils.prepend(returnedFilename));
const imageSummary = Docs.Create.ImageDocument(url, {
x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
- width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
+ _width: 150, _height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
});
imageSummary.isButton = true;
this.props.addDocument && this.props.addDocument(imageSummary);
@@ -151,12 +151,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
if (this.youtubeVideoId) {
const youtubeaspect = 400 / 315;
- const nativeWidth = (this.Document.nativeWidth || 0);
- const nativeHeight = (this.Document.nativeHeight || 0);
+ const nativeWidth = (this.Document._nativeWidth || 0);
+ const nativeHeight = (this.Document._nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
- if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
- this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect;
- this.Document.height = (this.Document.width || 0) / youtubeaspect;
+ if (!this.Document._nativeWidth) this.Document._nativeWidth = 600;
+ this.Document._nativeHeight = (this.Document._nativeWidth || 0) / youtubeaspect;
+ this.Document._height = (this.Document._width || 0) / youtubeaspect;
}
}
}
@@ -321,7 +321,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
const start = untracked(() => Math.round(this.Document.currentTimecode || 0));
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
+ onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document._nativeWidth || 640)} height={(this.Document._nativeHeight || 390)}
src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
}
@@ -340,7 +340,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.props.PanelWidth}
- annotationsKey={this.annotationsKey}
+ annotationsKey={this.annotationKey}
focus={this.props.focus}
isSelected={this.props.isSelected}
isAnnotationOverlay={true}
@@ -353,7 +353,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
addDocument={this.addDocumentWithTimestamp}
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
chromeCollapsed={true}>
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index b35ea0bb0..a48dc286e 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -34,17 +34,17 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
@observable private collapsed: boolean = true;
@observable private url: string = "";
- componentWillMount() {
+ componentDidMount() {
const field = Cast(this.props.Document[this.props.fieldKey], WebField);
if (field && field.url.href.indexOf("youtube") !== -1) {
const youtubeaspect = 400 / 315;
- const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
- const nativeHeight = NumCast(this.layoutDoc.nativeHeight);
+ const nativeWidth = NumCast(this.layoutDoc._nativeWidth);
+ const nativeHeight = NumCast(this.layoutDoc._nativeHeight);
if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
- if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
- this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
- this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
+ if (!nativeWidth) this.layoutDoc._nativeWidth = 600;
+ this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect;
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
}
}
@@ -83,13 +83,12 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
const field = Cast(this.props.Document[this.props.fieldKey], WebField);
if (field) url = field.url.href;
- const newBox = Docs.Create.TextDocument({
+ const newBox = Docs.Create.TextDocument(url, {
x: NumCast(this.props.Document.x),
y: NumCast(this.props.Document.y),
title: url,
- width: 200,
- height: 70,
- documentText: "@@@" + url
+ _width: 200,
+ _height: 70,
});
SelectionManager.SelectedDocuments().map(dv => {
@@ -198,7 +197,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.props.PanelWidth}
- annotationsKey={this.annotationsKey}
+ annotationsKey={this.annotationKey}
focus={this.props.focus}
isSelected={this.props.isSelected}
isAnnotationOverlay={true}
@@ -211,7 +210,6 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
addDocument={this.addDocument}
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
chromeCollapsed={true}>
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 6599c0e3c..d8b340db6 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -11,10 +11,11 @@ import "./Annotation.scss";
interface IAnnotationProps {
anno: Doc;
- extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
pinToPres: (document: Doc) => void;
focus: (doc: Doc) => void;
+ dataDoc: Doc;
+ fieldKey: string;
}
export default class Annotation extends React.Component<IAnnotationProps> {
@@ -29,10 +30,11 @@ interface IRegionAnnotationProps {
y: number;
width: number;
height: number;
- extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
document: Doc;
+ dataDoc: Doc;
+ fieldKey: string;
}
@observer
@@ -62,12 +64,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
}
deleteAnnotation = () => {
- const annotation = DocListCast(this.props.extensionDoc.annotations);
+ const annotation = DocListCast(this.props.dataDoc[this.props.fieldKey + "-annotations"]);
const group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
if (annotation.indexOf(group) !== -1) {
const newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
- this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
+ this.props.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>(newAnnotations);
}
DocListCast(group.annotations).forEach(anno => anno.delete = true);
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 503696ae9..05c70b74a 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -98,7 +98,7 @@ export default class PDFMenu extends AntimodeMenu {
}
render() {
- const buttons = this.Status === "pdf" ?
+ const buttons = this.Status === "pdf" ?
[
<button key="1" className="antimodeMenu-button" title="Click to Highlight" onClick={this.highlightClicked} style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} /></button>,
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 62467ce4d..a7c1990e9 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -113,8 +113,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
private _coverPath: any;
@computed get allAnnotations() {
- return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
- anno => this._script.run({ this: anno }, console.log, true).result) : [];
+ return DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]).filter(
+ anno => this._script.run({ this: anno }, console.log, true).result);
}
@computed get nonDocAnnotations() {
@@ -208,8 +208,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
await this.initialLoad();
this._annotationReactionDisposer = reaction(
- () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
- annotations => annotations && annotations.length && (this._annotations = annotations),
+ () => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]),
+ annotations => annotations?.length && (this._annotations = annotations),
{ fireImmediately: true });
this._filterReactionDisposer = reaction(
@@ -271,8 +271,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
- if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
- if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
+ if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+ if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
annoDoc.group = mainAnnoDoc;
annoDoc.isButton = true;
annoDocs.push(annoDoc);
@@ -286,8 +286,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
const annoDoc = new Doc();
if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
- if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
- if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
+ if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
+ if (anno.style.width) annoDoc._width = parseInt(anno.style.width);
annoDoc.group = mainAnnoDoc;
annoDoc.backgroundColor = color;
annoDocs.push(annoDoc);
@@ -558,7 +558,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
startDrag = (e: PointerEvent, ele: HTMLElement): void => {
e.preventDefault();
e.stopPropagation();
- const targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+ const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
if (annotationDoc) {
DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
@@ -573,11 +573,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
data.title = StrCast(data.title) + "_snippet";
view.proto = data;
- view.nativeHeight = marquee.height;
- view.height = (this.Document[WidthSym]() / (this.Document.nativeWidth || 1)) * marquee.height;
- view.nativeWidth = this.Document.nativeWidth;
+ view._nativeHeight = marquee.height;
+ view._height = (this.Document[WidthSym]() / (this.Document._nativeWidth || 1)) * marquee.height;
+ view._nativeWidth = this.Document._nativeWidth;
view.startY = marquee.top;
- view.width = this.Document[WidthSym]();
+ view._width = this.Document[WidthSym]();
DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
}
@@ -598,12 +598,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
getCoverImage = () => {
if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
setTimeout((() => {
- this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
- this.Document.nativeHeight = (this.Document.nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
+ this.Document._height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
+ this.Document._nativeHeight = (this.Document._nativeWidth || 0) * this._coverPath.height / this._coverPath.width;
}).bind(this), 0);
}
- const nativeWidth = (this.Document.nativeWidth || 0);
- const nativeHeight = (this.Document.nativeHeight || 0);
+ const nativeWidth = (this.Document._nativeWidth || 0);
+ const nativeHeight = (this.Document._nativeHeight || 0);
const resolved = Utils.prepend(this._coverPath.path);
return <img key={resolved} src={resolved} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
@@ -621,19 +621,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
@computed get annotationLayer() {
TraceMobx();
- return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
+ return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document.nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc!} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)}
</div>;
}
overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
- panelWidth = () => (this.Document.scrollHeight || this.Document.nativeHeight || 0);
- panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0);
+ panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
+ panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
@computed get overlayLayer() {
return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
<CollectionFreeFormView {...this.props}
LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? []}
- annotationsKey={this.annotationsKey}
+ annotationsKey={this.annotationKey}
setPreviewCursor={this.setPreviewCursor}
PanelHeight={this.panelWidth}
PanelWidth={this.panelHeight}
@@ -650,7 +650,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
addDocument={this.addDocument}
CollectionView={undefined}
ScreenToLocalTransform={this.overlayTransform}
- ruleProvider={undefined}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}
chromeCollapsed={true}>
@@ -676,20 +675,19 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
contentZoom = () => this._zoomed;
render() {
TraceMobx();
- return !this.extensionDoc ? (null) :
- <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
- onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
- style={{
- width: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeWidth) : `${100 / this.contentScaling}%`,
- height: !this.props.Document.fitWidth ? NumCast(this.props.Document.nativeHeight) : `${100 / this.contentScaling}%`,
- transform: `scale(${this.props.ContentScaling()})`
- }} >
- {this.pdfViewerDiv}
- {this.overlayLayer}
- {this.annotationLayer}
- {this.standinViews}
- <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
- </div >;
+ return <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
+ onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
+ style={{
+ width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
+ height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
+ transform: `scale(${this.props.ContentScaling()})`
+ }} >
+ {this.pdfViewerDiv}
+ {this.overlayLayer}
+ {this.annotationLayer}
+ {this.standinViews}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+ </div >;
}
}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 37c837414..dad55e1fd 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -161,12 +161,12 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
return (null);
}
- const propDocWidth = NumCast(this.layoutDoc.nativeWidth);
- const propDocHeight = NumCast(this.layoutDoc.nativeHeight);
- const scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
+ const propDocWidth = NumCast(this.layoutDoc._nativeWidth);
+ const propDocHeight = NumCast(this.layoutDoc._nativeHeight);
+ const scale = () => 175 / NumCast(this.layoutDoc._nativeWidth, 175);
return (
<div className="presElementBox-embedded" style={{
- height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
+ height: propDocHeight === 0 ? NumCast(this.layoutDoc._height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
}}>
<ContentFittingDocumentView
@@ -175,12 +175,10 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
addDocument={returnFalse}
removeDocument={returnFalse}
- ruleProvider={undefined}
addDocTab={returnFalse}
pinToPres={returnFalse}
PanelWidth={() => this.props.PanelWidth() - 20}
PanelHeight={() => 100}
- setPreviewScript={emptyFunction}
getTransform={Transform.Identity}
active={this.props.active}
moveDocument={this.props.moveDocument!}
@@ -194,7 +192,7 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
}
render() {
- const treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
+ const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
const pbi = "presElementBox-interaction";
return (
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 0825580b7..f492ea773 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -9,6 +9,7 @@
position: absolute;
font-size: 10px;
line-height: 1;
+ overflow: hidden;
}
.searchBox-bar {
height: 32px;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index dd1ac7421..be13dae03 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -217,16 +217,16 @@ export class SearchBox extends React.Component {
doc.x = x;
doc.y = y;
const size = 200;
- const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
+ const aspect = NumCast(doc._nativeHeight) / NumCast(doc._nativeWidth, 1);
if (aspect > 1) {
- doc.height = size;
- doc.width = size / aspect;
+ doc._height = size;
+ doc._width = size / aspect;
} else if (aspect > 0) {
- doc.width = size;
- doc.height = size * aspect;
+ doc._width = size;
+ doc._height = size * aspect;
} else {
- doc.width = size;
- doc.height = size;
+ doc._width = size;
+ doc._height = size;
}
x += 250;
if (x > 1000) {
@@ -234,7 +234,7 @@ export class SearchBox extends React.Component {
y += 300;
}
}
- return Docs.Create.TreeDocument(docs, { width: 200, height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
+ return Docs.Create.TreeDocument(docs, { _width: 200, _height: 400, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` });
}
@action.bound
@@ -267,10 +267,11 @@ export class SearchBox extends React.Component {
@action
resultsScrolled = (e?: React.UIEvent<HTMLDivElement>) => {
+ if (!this.resultsRef.current) return;
const scrollY = e ? e.currentTarget.scrollTop : this.resultsRef.current ? this.resultsRef.current.scrollTop : 0;
const itemHght = 53;
const startIndex = Math.floor(Math.max(0, scrollY / itemHght));
- const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current!.getBoundingClientRect().height / itemHght)));
+ const endIndex = Math.ceil(Math.min(this._numTotalResults - 1, startIndex + (this.resultsRef.current.getBoundingClientRect().height / itemHght)));
this._endIndex = endIndex === -1 ? 12 : endIndex;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 32ba5d19d..8aea737f0 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -68,11 +68,11 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
getOnClick({ col, target }: { col: Doc, target: Doc }) {
return () => {
col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
- if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(target.x) + NumCast(target.width) / 2;
- const newPanY = NumCast(target.y) + NumCast(target.height) / 2;
- col.panX = newPanX;
- col.panY = newPanY;
+ if (NumCast(col._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target._width) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target._height) / 2;
+ col._panX = newPanX;
+ col._panY = newPanY;
}
CollectionDockingView.AddRightSplit(col, undefined);
};
@@ -161,7 +161,6 @@ export class SearchItem extends React.Component<SearchItemProps> {
fitToBox={StrCast(this.props.doc.type).indexOf(DocumentType.COL) !== -1}
addDocument={returnFalse}
removeDocument={returnFalse}
- ruleProvider={undefined}
addDocTab={returnFalse}
pinToPres={returnFalse}
getTransform={Transform.Identity}
@@ -172,8 +171,6 @@ export class SearchItem extends React.Component<SearchItemProps> {
moveDocument={returnFalse}
active={returnFalse}
whenActiveChanged={returnFalse}
- setPreviewScript={emptyFunction}
- previewScript={undefined}
/>
</div>;
return docview;
@@ -262,7 +259,6 @@ export class SearchItem extends React.Component<SearchItemProps> {
onPointerMoved = (e: PointerEvent) => {
if (Math.abs(e.clientX - this._downX) > Utils.DRAG_THRESHOLD ||
Math.abs(e.clientY - this._downY) > Utils.DRAG_THRESHOLD) {
- console.log("DRAGGIGNG");
document.removeEventListener("pointermove", this.onPointerMoved);
document.removeEventListener("pointerup", this.onPointerUp);
const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc;
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 0b0280519..1583e3d5d 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -45,7 +45,7 @@ class Uploader extends React.Component {
const formData = new FormData();
formData.append("file", files[0]);
- const upload = window.location.origin + "/upload";
+ const upload = window.location.origin + "/uploadFormData";
this.status = "uploading image";
const res = await fetch(upload, {
method: 'POST',
@@ -55,7 +55,7 @@ class Uploader extends React.Component {
const json = await res.json();
json.map(async (file: any) => {
const path = window.location.origin + file;
- const doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name });
+ const doc = Docs.Create.ImageDocument(path, { _nativeWidth: 200, _width: 200, title: name });
this.status = "getting user document";
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index ea2fc917f..b1eaeaa0a 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -38,7 +38,6 @@ export default class MobileInterface extends React.Component {
addDocTab={returnFalse}
pinToPres={emptyFunction}
removeDocument={undefined}
- ruleProvider={undefined}
onClick={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts
index fd86031a8..28467377b 100644
--- a/src/new_fields/CursorField.ts
+++ b/src/new_fields/CursorField.ts
@@ -2,7 +2,7 @@ import { ObjectField } from "./ObjectField";
import { observable } from "mobx";
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, createSimpleSchema, object, date } from "serializr";
-import { OnUpdate, ToScriptString, Copy } from "./FieldSymbols";
+import { OnUpdate, ToScriptString, ToString, Copy } from "./FieldSymbols";
export type CursorPosition = {
x: number,
@@ -60,4 +60,7 @@ export default class CursorField extends ObjectField {
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return "invalid";
+ }
} \ No newline at end of file
diff --git a/src/new_fields/DateField.ts b/src/new_fields/DateField.ts
index 4f999e5e8..a925148c2 100644
--- a/src/new_fields/DateField.ts
+++ b/src/new_fields/DateField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, date } from "serializr";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
import { scriptingGlobal, Scripting } from "../client/util/Scripting";
@scriptingGlobal
@@ -26,6 +26,9 @@ export class DateField extends ObjectField {
[ToScriptString]() {
return `new DateField(new Date(${this.date.toISOString()}))`;
}
+ [ToString]() {
+ return this.date.toISOString();
+ }
}
Scripting.addGlobal(function d(...dateArgs: any[]) {
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 8e0b28606..5f78636a9 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,21 +1,22 @@
-import { observable, ObservableMap, runInAction, action, untracked } from "mobx";
+import { observable, ObservableMap, runInAction } from "mobx";
import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
-import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, Update } from "./FieldSymbols";
+import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
import { List } from "./List";
import { ObjectField } from "./ObjectField";
import { PrefetchProxy, ProxyField } from "./Proxy";
import { FieldId, RefField } from "./RefField";
import { listSpec } from "./Schema";
-import { ComputedField } from "./ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
+import { ComputedField, ScriptField } from "./ScriptField";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
import { intersectRect } from "../Utils";
import { UndoManager } from "../client/util/UndoManager";
import { computedFn } from "mobx-utils";
+import { RichTextField } from "./RichTextField";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -36,6 +37,18 @@ export namespace Field {
return field[ToScriptString]();
}
}
+ export function toString(field: Field): string {
+ if (typeof field === "string") {
+ return field;
+ } else if (typeof field === "number" || typeof field === "boolean") {
+ return String(field);
+ } else if (field instanceof ObjectField) {
+ return field[ToString]();
+ } else if (field instanceof RefField) {
+ return field[ToString]();
+ }
+ return "(null)";
+ }
export function IsField(field: any): field is Field;
export function IsField(field: any, includeUndefined: true): field is Field | undefined;
export function IsField(field: any, includeUndefined: boolean = false): field is Field | undefined {
@@ -75,6 +88,7 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
+export const DataSym = Symbol("Data");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
const CachedUpdates = Symbol("Cached updates");
@@ -150,12 +164,16 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [WidthSym] = () => NumCast(this[SelfProxy].width);
- public [HeightSym] = () => NumCast(this[SelfProxy].height);
+ public [WidthSym] = () => NumCast(this[SelfProxy]._width);
+ public [HeightSym] = () => NumCast(this[SelfProxy]._height);
+ public get [DataSym]() { return Cast(this[SelfProxy].resolvedDataDoc, Doc, null) || this[SelfProxy]; }
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return "Doc";
+ }
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
@@ -290,14 +308,13 @@ export namespace Doc {
* @param fields the fields to project onto the target. Its type signature defines a mapping from some string key
* to a potentially undefined field, where each entry in this mapping is optional.
*/
- export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) {
+ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const value = fields[key];
- // Do we want to filter out undefineds?
- // if (value !== undefined) {
- doc[key] = value;
- // }
+ if (!skipUndefineds || value !== undefined) { // Do we want to filter out undefineds?
+ doc[key] = value;
+ }
}
}
return doc;
@@ -406,88 +423,74 @@ export namespace Doc {
}
export function MakeAlias(doc: Doc) {
const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
- if (alias.layout instanceof Doc) {
- alias.layout = Doc.MakeAlias(alias.layout);
+ const layout = Doc.Layout(alias);
+ if (layout instanceof Doc && layout !== alias) {
+ Doc.SetLayout(alias, Doc.MakeAlias(layout));
}
- const aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
- alias.title = ComputedField.MakeFunction(`renameAlias(this, ${aliasNumber})`);
+ alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`);
return alias;
}
//
// Determines whether the combination of the layoutDoc and dataDoc represents
- // a template relationship. If so, the layoutDoc will be 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.
+ // a template relationship : there is a dataDoc and it doesn't match the layoutDoc an
+ // the lyouatDoc's layout is layout string (not a document)
//
export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc[StrCast(layoutDoc.layoutKey, "layout")] instanceof Doc);
+ return layoutDoc.isTemplateForField && dataDoc && layoutDoc !== dataDoc && !(Doc.LayoutField(layoutDoc) instanceof Doc);
}
//
- // Returns an expanded template layout for a target data document.
- // 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
+ // 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.
//
export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;
- // if we have a data doc that doesn't match the layout, then we're rendering a template.
- // ... which means we change the layout to be an expanded view of the template layout.
- // This allows the view override the template's properties and be referenceable as its own document.
- const expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
- const expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
- if (expandedTemplateLayout instanceof Doc) {
- return expandedTemplateLayout;
- }
+ const templateField = StrCast(templateLayoutDoc.isTemplateForField); // 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 expandedLayoutFieldKey = templateField + "-layout[" + templateLayoutDoc[Id] + "]";
+ const expandedTemplateLayout = dataDoc?.[expandedLayoutFieldKey];
if (expandedTemplateLayout === undefined) {
- setTimeout(() => dataDoc[expandedLayoutFieldKey] === undefined &&
- (dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]")), 0);
+ setTimeout(() => {
+ if (!dataDoc[expandedLayoutFieldKey]) {
+ const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
+ dataDoc[expandedLayoutFieldKey] = newLayoutDoc;
+ newLayoutDoc.resolvedDataDoc = dataDoc;
+ if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) {
+ dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc });
+ }
+ }
+ }, 0);
}
- return undefined; // use the templateLayout when it's not a template or the expandedTemplate is pending.
+ return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplate is pending.
}
- export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
- let layoutDoc: Doc | undefined = childDocLayout;
- const resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
- if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
- const extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
- layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
- } else layoutDoc = childDocLayout;
- return { layout: layoutDoc, data: resolvedDataDoc };
+ // if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
+ // otherwise, it just returns the childDoc
+ export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
+ const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : Doc.GetDataDoc(containerDataDoc);
+ return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
}
-
- //
- // Resolves a reference to a field by returning 'doc' if no field extension is specified,
- // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
- // This mechanism allows any fields to be extended with an extension document that can
- // be used to capture field-specific metadata. For example, an image field can be extended
- // to store annotations, ink, and other data.
- //
- export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
- const extension = doc[fieldKey + "_ext"];
- if (extension === undefined) {
- setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
- }
- return extension ? extension as Doc : undefined;
- }
- export function fieldExtensionDocSync(doc: Doc, fieldKey: string) {
- return (doc[fieldKey + "_ext"] as Doc) || CreateDocumentExtensionForField(doc, fieldKey);
- }
-
export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- const docExtensionForField = new Doc(doc[Id] + fieldKey, true);
- docExtensionForField.title = fieldKey + ".ext"; // courtesy field--- shouldn't be needed except maybe for debugging
- docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
- docExtensionForField.extendsField = fieldKey; // this can be used by search to map matches on the extension doc back to the field that was extended.
- docExtensionForField.type = DocumentType.EXTENSION;
let proto: Doc | undefined = doc;
while (proto && !Doc.IsPrototype(proto) && proto.proto) {
proto = proto.proto;
}
- (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ let docExtensionForField = ((proto || doc)[fieldKey + "_ext"] as Doc);
+ if (!docExtensionForField) {
+ docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = fieldKey + ".ext"; // courtesy field--- shouldn't be needed except maybe for debugging
+ docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+ docExtensionForField.extendsField = fieldKey; // this can be used by search to map matches on the extension doc back to the field that was extended.
+ docExtensionForField.type = DocumentType.EXTENSION;
+ (proto || doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ }
return docExtensionForField;
}
@@ -516,7 +519,9 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
const copy = new Doc(copyProtoId, true);
+ const exclude = Cast(doc.excludeFields, listSpec("string"), []);
Object.keys(doc).forEach(key => {
+ if (exclude.includes(key)) return;
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === "proto" && copyProto) {
@@ -565,57 +570,64 @@ export namespace Doc {
export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
if (!templateDoc) {
target.layout = undefined;
- target.nativeWidth = undefined;
- target.nativeHeight = undefined;
+ target._nativeWidth = undefined;
+ target._nativeHeight = undefined;
target.onClick = undefined;
target.type = undefined;
return;
}
- const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
+ if ((target[targetKey] as Doc)?.proto !== templateDoc) {
+ const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
- titleTarget && (Doc.GetProto(target).title = titleTarget);
- Doc.GetProto(target).type = DocumentType.TEMPLATE;
- target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
+ Doc.GetProto(target).type = DocumentType.TEMPLATE;
+ target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
- Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+ Doc.GetProto(target)[targetKey] = new PrefetchProxy(layoutCustomLayout);
+ }
target.layoutKey = targetKey;
return target;
}
- export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean {
- // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
- const metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
- let fieldLayoutDoc = fieldTemplate;
- if (fieldTemplate.layout instanceof Doc) {
- fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
- }
-
- fieldTemplate.templateField = metadataFieldName;
- fieldTemplate.title = metadataFieldName;
- fieldTemplate.isTemplateField = true;
- /* move certain layout properties from the original data doc to the template layout to avoid
- inheriting them from the template's data doc which may also define these fields for its own use.
- */
- fieldTemplate.ignoreAspect = fieldTemplate.ignoreAspect === undefined ? undefined : BoolCast(fieldTemplate.ignoreAspect);
- fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
- fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
- fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
- fieldTemplate.type = fieldTemplate.type;
- fieldTemplate.panX = 0;
- fieldTemplate.panY = 0;
- fieldTemplate.scale = 1;
- fieldTemplate.showTitle = suppressTitle ? undefined : "title";
- const data = fieldTemplate.data;
- // setTimeout(action(() => {
- !templateDataDoc[metadataFieldName] && data instanceof ObjectField && (Doc.GetProto(templateDataDoc)[metadataFieldName] = ObjectField.MakeCopy(data));
- const layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldName}'}`);
- const layoutDelegate = Doc.Layout(fieldTemplate);
- layoutDelegate.layout = layout;
- fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
- if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
- fieldTemplate.proto = templateDataDoc;
- // }), 0);
+ //
+ // This function converts a generic field layout display into a field layout that displays a specific
+ // metadata field indicated by the title of the template field (not the default field that it was rendering)
+ //
+ export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
+
+ // find the metadata field key that this template field doc will display (indicated by its title)
+ const metadataFieldKey = StrCast(templateField.title).replace(/^-/, "");
+
+ // update the original template to mark it as a template
+ templateField.isTemplateForField = metadataFieldKey;
+ templateField.title = metadataFieldKey;
+
+ // move any data that the template field had been rendering over to the template doc so that things will still be rendered
+ // when the template field is adjusted to point to the new metadatafield key.
+ // note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well.
+ // note 2: this will not overwrite any field that already exists on the template doc at the field key
+ if (!templateDoc?.[metadataFieldKey] && templateField.data instanceof ObjectField) {
+ Cast(templateField.data, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
+ (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateField.data));
+ }
+ if (templateField.data instanceof RichTextField && templateField.data.Text) {
+ templateField._textTemplate = ComputedField.MakeFunction(`copyField(this.${metadataFieldKey})`, { this: Doc.name });
+ }
+
+ // get the layout string that the template uses to specify its layout
+ const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField)));
+
+ // change itto render the target metadata field instead of what it was rendering before and assign it to the template field layout document.
+ Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`);
+
+ // assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino)
+ Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc);
+
+ if (templateField.backgroundColor !== templateDoc?.defaultBackgroundColor) {
+ templateField.defaultBackgroundColor = templateField.backgroundColor;
+ }
+
return true;
}
@@ -624,12 +636,12 @@ export namespace Doc {
const doc1Layout = Doc.Layout(doc1);
const x2 = NumCast(doc2.x) - clusterDistance;
const y2 = NumCast(doc2.y) - clusterDistance;
- const w2 = NumCast(doc2Layout.width) + clusterDistance;
- const h2 = NumCast(doc2Layout.height) + clusterDistance;
+ const w2 = NumCast(doc2Layout._width) + clusterDistance;
+ const h2 = NumCast(doc2Layout._height) + clusterDistance;
const x = NumCast(doc1.x) - clusterDistance;
const y = NumCast(doc1.y) - clusterDistance;
- const w = NumCast(doc1Layout.width) + clusterDistance;
- const h = NumCast(doc1Layout.height) + clusterDistance;
+ const w = NumCast(doc1Layout._width) + clusterDistance;
+ const h = NumCast(doc1Layout._height) + clusterDistance;
return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
}
@@ -654,8 +666,10 @@ export namespace Doc {
// the document containing the view layout information - will be the Document itself unless the Document has
// a layout field. In that case, all layout information comes from there unless overriden by Document
- export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
+ export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? Doc.LayoutField(doc) as Doc : doc; }
+ export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
+ export function LayoutFieldKey(doc: Doc) { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
const manager = new DocData();
export function SearchQuery(): string { return manager._searchQuery; }
export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
@@ -688,7 +702,7 @@ export namespace Doc {
export function LinkOtherAnchor(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? Cast(linkDoc.anchor2, Doc) as Doc : Cast(linkDoc.anchor1, Doc) as Doc; }
- export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
+ export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layout_key1" : "layout_key2"; }
export function linkFollowUnhighlight() {
Doc.UnhighlightAll();
@@ -696,9 +710,9 @@ export namespace Doc {
}
let dt = 0;
- export function linkFollowHighlight(destDoc: Doc) {
+ export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) {
linkFollowUnhighlight();
- Doc.HighlightDoc(destDoc);
+ Doc.HighlightDoc(destDoc, dataAndDisplayDocs);
document.removeEventListener("pointerdown", linkFollowUnhighlight);
document.addEventListener("pointerdown", linkFollowUnhighlight);
const x = dt = Date.now();
@@ -712,10 +726,10 @@ export namespace Doc {
export function IsHighlighted(doc: Doc) {
return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
}
- export function HighlightDoc(doc: Doc) {
+ export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
runInAction(() => {
highlightManager.HighlightedDoc.set(doc, true);
- highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true);
+ dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true);
});
}
export function UnHighlightDoc(doc: Doc) {
@@ -742,12 +756,27 @@ export namespace Doc {
source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
}
-}
+ export function setChildDetailedLayout(target: Doc, source?: Doc) {
+ target.childDetailed = source && source.isTemplateDoc ? source : source &&
+ source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
+ source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
+ }
+ export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
+ const fieldVal = doc[key] ? doc[key] : doc[key + "_ext"];
+ if (Cast(fieldVal, listSpec("string"), []).length) {
+ const vals = Cast(fieldVal, listSpec("string"), []);
+ return vals.some(v => v === value);
+ }
+ const fieldStr = Field.toString(fieldVal as Field);
+ return fieldStr === value;
+ }
+}
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
Scripting.addGlobal(function setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); });
+Scripting.addGlobal(function setChildDetailedLayout(target: any, source: any) { Doc.setChildDetailedLayout(target, source); });
Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
@@ -760,4 +789,19 @@ Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs =
Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)));
return docs.length ? new List(docs) : prevValue;
+});
+Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) {
+ const docFilters = Cast(container._docFilter, listSpec("string"), []);
+ for (let i = 0; i < docFilters.length; i += 3) {
+ if (docFilters[i] === key && docFilters[i + 1] === value) {
+ docFilters.splice(i, 3);
+ break;
+ }
+ }
+ if (modifiers !== undefined) {
+ docFilters.push(key);
+ docFilters.push(value);
+ docFilters.push(modifiers);
+ container._docFilter = new List<string>(docFilters);
+ }
}); \ No newline at end of file
diff --git a/src/new_fields/FieldSymbols.ts b/src/new_fields/FieldSymbols.ts
index b5b3aa588..4aadb81a2 100644
--- a/src/new_fields/FieldSymbols.ts
+++ b/src/new_fields/FieldSymbols.ts
@@ -7,4 +7,5 @@ export const Id = Symbol("Id");
export const OnUpdate = Symbol("OnUpdate");
export const Parent = Symbol("Parent");
export const Copy = Symbol("Copy");
-export const ToScriptString = Symbol("ToScriptString"); \ No newline at end of file
+export const ToScriptString = Symbol("ToScriptString");
+export const ToString = Symbol("ToString"); \ No newline at end of file
diff --git a/src/new_fields/HtmlField.ts b/src/new_fields/HtmlField.ts
index f952acff9..6e8bba977 100644
--- a/src/new_fields/HtmlField.ts
+++ b/src/new_fields/HtmlField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString} from "./FieldSymbols";
@Deserializable("html")
export class HtmlField extends ObjectField {
@@ -20,4 +20,7 @@ export class HtmlField extends ObjectField {
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return this.html;
+ }
}
diff --git a/src/new_fields/IconField.ts b/src/new_fields/IconField.ts
index 62b2cd254..76c4ddf1b 100644
--- a/src/new_fields/IconField.ts
+++ b/src/new_fields/IconField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
@Deserializable("icon")
export class IconField extends ObjectField {
@@ -20,4 +20,7 @@ export class IconField extends ObjectField {
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return "ICONfield";
+ }
}
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 20dbf4b17..4a44b4f55 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -1,8 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom, createSimpleSchema, list, object, map } from "serializr";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
-import { DeepCopy } from "../Utils";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
export enum InkTool {
None,
@@ -46,4 +45,7 @@ export class InkField extends ObjectField {
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return "InkField";
+ }
}
diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts
index bb48b1bb3..a43f11e82 100644
--- a/src/new_fields/List.ts
+++ b/src/new_fields/List.ts
@@ -6,7 +6,7 @@ import { observable, action } from "mobx";
import { ObjectField } from "./ObjectField";
import { RefField } from "./RefField";
import { ProxyField } from "./Proxy";
-import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, Copy } from "./FieldSymbols";
+import { Self, Update, Parent, OnUpdate, SelfProxy, ToScriptString, ToString, Copy } from "./FieldSymbols";
import { Scripting } from "../client/util/Scripting";
const listHandlers: any = {
@@ -292,6 +292,9 @@ class ListImpl<T extends Field> extends ObjectField {
[ToScriptString]() {
return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
}
+ [ToString]() {
+ return "List";
+ }
}
export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[];
export const List: { new <T extends Field>(fields?: T[]): List<T> } = ListImpl as any;
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index 65ada91c0..566104b40 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,6 +1,6 @@
import { Doc } from "./Doc";
import { RefField } from "./RefField";
-import { OnUpdate, Parent, Copy, ToScriptString } from "./FieldSymbols";
+import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols";
import { Scripting } from "../client/util/Scripting";
export abstract class ObjectField {
@@ -9,11 +9,12 @@ export abstract class ObjectField {
abstract [Copy](): ObjectField;
abstract [ToScriptString](): string;
+ abstract [ToString](): string;
}
export namespace ObjectField {
export function MakeCopy<T extends ObjectField>(field: T) {
- return field[Copy]();
+ return field?.[Copy]();
}
}
diff --git a/src/new_fields/Proxy.ts b/src/new_fields/Proxy.ts
index c6292e37c..d50c0f14e 100644
--- a/src/new_fields/Proxy.ts
+++ b/src/new_fields/Proxy.ts
@@ -5,7 +5,7 @@ import { observable, action } from "mobx";
import { DocServer } from "../client/DocServer";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
-import { Id, Copy, ToScriptString } from "./FieldSymbols";
+import { Id, Copy, ToScriptString, ToString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { Plugins } from "./util";
@@ -32,6 +32,9 @@ export class ProxyField<T extends RefField> extends ObjectField {
[ToScriptString]() {
return "invalid";
}
+ [ToString]() {
+ return "ProxyField";
+ }
@serializable(primitive())
readonly fieldId: string = "";
diff --git a/src/new_fields/RefField.ts b/src/new_fields/RefField.ts
index f7bea8c94..b6ef69750 100644
--- a/src/new_fields/RefField.ts
+++ b/src/new_fields/RefField.ts
@@ -1,6 +1,6 @@
import { serializable, primitive, alias } from "serializr";
import { Utils } from "../Utils";
-import { Id, HandleUpdate, ToScriptString } from "./FieldSymbols";
+import { Id, HandleUpdate, ToScriptString, ToString } from "./FieldSymbols";
import { afterDocDeserialize } from "../client/util/SerializationHelper";
export type FieldId = string;
@@ -17,4 +17,5 @@ export abstract class RefField {
protected [HandleUpdate]?(diff: any): void | Promise<void>;
abstract [ToScriptString](): string;
+ abstract [ToString](): string;
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index fd5459876..a0f21f45e 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -1,7 +1,7 @@
import { ObjectField } from "./ObjectField";
import { serializable } from "serializr";
import { Deserializable } from "../client/util/SerializationHelper";
-import { Copy, ToScriptString } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
@scriptingGlobal
@@ -26,5 +26,8 @@ export class RichTextField extends ObjectField {
[ToScriptString]() {
return `new RichTextField("${this.Data}", "${this.Text}")`;
}
+ [ToString]() {
+ return this.Text;
+ }
} \ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 682206aa2..c50f8cc48 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -273,8 +273,8 @@ export namespace RichTextUtils {
const guid = Utils.GenerateDeterministicGuid(src);
const backingDocId = StrCast(textNote[guid]);
if (!backingDocId) {
- const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
- DocumentView.makeCustomViewClicked(backingDoc, undefined);
+ const backingDoc = Docs.Create.ImageDocument(src, { _width: 300, _height: 300 });
+ DocumentView.makeCustomViewClicked(backingDoc, undefined, Docs.Create.FreeformDocument);
docid = backingDoc[Id];
textNote[guid] = docid;
} else {
@@ -403,7 +403,7 @@ export namespace RichTextUtils {
let exported = (await Cast(linkDoc.anchor2, Doc))!;
if (!exported.customLayout) {
exported = Doc.MakeAlias(exported);
- DocumentView.makeCustomViewClicked(exported, undefined);
+ DocumentView.makeCustomViewClicked(exported, undefined, Docs.Create.FreeformDocument);
linkDoc.anchor2 = exported;
}
url = Utils.shareUrl(exported[Id]);
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
index 42a8485ac..07c90f5a2 100644
--- a/src/new_fields/SchemaHeaderField.ts
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, primitive } from "serializr";
import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, OnUpdate } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
import { ColumnType } from "../client/views/collections/CollectionSchemaView";
@@ -116,4 +116,7 @@ export class SchemaHeaderField extends ObjectField {
[ToScriptString]() {
return `invalid`;
}
+ [ToString]() {
+ return `SchemaHeaderField`;
+ }
} \ No newline at end of file
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index b5ad4a7f6..f8a8d1226 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -1,9 +1,9 @@
import { ObjectField } from "./ObjectField";
import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions } from "../client/util/Scripting";
-import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
+import { Copy, ToScriptString, ToString, Parent, SelfProxy } from "./FieldSymbols";
import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
import { Deserializable, autoObject } from "../client/util/SerializationHelper";
-import { Doc } from "../new_fields/Doc";
+import { Doc, Field } from "../new_fields/Doc";
import { Plugins } from "./util";
import { computedFn } from "mobx-utils";
import { ProxyField } from "./Proxy";
@@ -101,12 +101,16 @@ export class ScriptField extends ObjectField {
[ToScriptString]() {
return "script field";
}
- public static CompileScript(script: string, params: object = {}, addReturn = false) {
+ [ToString]() {
+ return "script field";
+ }
+ public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) {
const compiled = CompileScript(script, {
params: { this: Doc.name, _last_: "any", ...params },
typecheck: false,
editable: true,
- addReturn: addReturn
+ addReturn: addReturn,
+ capturedVariables
});
return compiled;
}
@@ -127,12 +131,12 @@ export class ComputedField extends ScriptField {
_lastComputedResult: any;
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
value = computedFn((doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, _last_: this._lastComputedResult }, console.log).result);
- public static MakeScript(script: string, params: object = {}, ) {
+ public static MakeScript(script: string, params: object = {}) {
const compiled = ScriptField.CompileScript(script, params, false);
return compiled.compiled ? new ComputedField(compiled) : undefined;
}
- public static MakeFunction(script: string, params: object = {}) {
- const compiled = ScriptField.CompileScript(script, params, true);
+ public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) {
+ const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ComputedField(compiled) : undefined;
}
}
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index 35ef6dd02..cfab36906 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from "../client/util/SerializationHelper";
import { serializable, custom } from "serializr";
import { ObjectField } from "./ObjectField";
-import { ToScriptString, Copy } from "./FieldSymbols";
+import { ToScriptString, ToString, Copy } from "./FieldSymbols";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
function url() {
@@ -32,6 +32,9 @@ export abstract class URLField extends ObjectField {
[ToScriptString]() {
return `new ${this.constructor.name}("${this.url.href}")`;
}
+ [ToString]() {
+ return this.url.href;
+ }
[Copy](): this {
return new (this.constructor as any)(this.url);
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 1cba3cba9..4a5c1fdb0 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -4,20 +4,22 @@ import { Doc } from "./Doc";
import { DateField } from "./DateField";
export const documentSchema = createSchema({
- layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below (see layoutCustom as an example)
+ layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below (see layout_custom as an example)
layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
- layoutCustom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
+ layout_custom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
title: "string", // document title (can be on either data document or layout)
- nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
- nativeHeight: "number", // "
- width: "number", // width of document in its container's coordinate system
- height: "number", // "
+ _dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
+ _nativeHeight: "number", // "
+ _width: "number", // width of document in its container's coordinate system
+ _height: "number", // "
+ _freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
+ _LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
color: "string", // foreground color of document
backgroundColor: "string", // background color of document
opacity: "number", // opacity of document
creationDate: DateField, // when the document was created
links: listSpec(Doc), // computed (readonly) list of links associated with this document
- dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
@@ -26,7 +28,7 @@ export const documentSchema = createSchema({
dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document
autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
- isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
+ isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field
isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
type: "string", // enumerated type of document
treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
@@ -43,7 +45,8 @@ export const documentSchema = createSchema({
searchFields: "string", // the search fields to display when this document matches a search in its metadata
heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
- showTitle: "string", // whether an editable title banner is displayed at tht top of the document
+ showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document
+ showTitleHover: "string", // the showTitle should be shown only on hover
isButton: "boolean", // whether document functions as a button (overiding native interactions of its content)
ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
isAnimating: "string", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). value is undefined|"min"|"max"
@@ -51,8 +54,12 @@ export const documentSchema = createSchema({
scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
strokeWidth: "number",
fontSize: "string",
+ fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
+ xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
LODarea: "number", // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
- LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
+ letterSpacing: "string",
+ textTransform: "string"
});
export const positionSchema = createSchema({
@@ -62,6 +69,13 @@ export const positionSchema = createSchema({
z: "number",
});
+export const collectionSchema = createSchema({
+ childLayout: Doc, // layout template for children of a collecion
+ childDetailed: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to use this field)
+ onChildClick: ScriptField, // script to run for each child when its clicked
+ onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view
+});
+
export type Document = makeInterface<[typeof documentSchema]>;
export const Document = makeInterface(documentSchema);
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 4147be278..2cedda7a6 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,12 +1,13 @@
import { UndoManager } from "../client/util/UndoManager";
import { Doc, Field, FieldResult, UpdatingFromServer } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
-import { ProxyField } from "./Proxy";
+import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
import { ObjectField } from "./ObjectField";
import { action, trace } from "mobx";
import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
+import { props } from "bluebird";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -35,6 +36,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
target[prop] = value;
return true;
}
+
if (typeof prop === "symbol") {
target[prop] = value;
return true;
@@ -52,7 +54,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
value = new ProxyField(value);
}
if (value instanceof ObjectField) {
- if (value[Parent] && value[Parent] !== receiver) {
+ if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
value[Parent] = receiver;
@@ -98,11 +100,38 @@ export function makeEditable() {
_setter = _setterImpl;
}
-export function setter(target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
+let layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
+ "LODdisable", "dropAction", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
+export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
+ let prop = in_prop;
+ if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
+ ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+ if (!prop.startsWith("_")) {
+ console.log(prop + " is deprecated - switch to _" + prop);
+ prop = "_" + prop;
+ }
+ const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver);
+ if (resolvedLayout instanceof Doc) {
+ resolvedLayout[prop] = value;
+ return true;
+ }
+ }
return _setter(target, prop, value, receiver);
}
-export function getter(target: any, prop: string | symbol | number, receiver: any): any {
+export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
+ let prop = in_prop;
+ if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
+ ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+ if (!prop.startsWith("_")) {
+ console.log(prop + " is deprecated - switch to _" + prop);
+ prop = "_" + prop;
+ }
+ const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver);
+ if (resolvedLayout instanceof Doc) {
+ return resolvedLayout[prop];
+ }
+ }
if (prop === "then") {//If we're being awaited
return undefined;
}
diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts
index 4b5ad6684..4e3493c1c 100644
--- a/src/pen-gestures/GestureUtils.ts
+++ b/src/pen-gestures/GestureUtils.ts
@@ -6,18 +6,16 @@ import { Doc, WidthSym, HeightSym } from "../new_fields/Doc";
import { NumCast } from "../new_fields/Types";
import { CollectionFreeFormView } from "../client/views/collections/collectionFreeForm/CollectionFreeFormView";
import { Rect } from "react-measure";
+import { Scripting } from "../client/util/Scripting";
export namespace GestureUtils {
- namespace GestureDataTypes {
- export type BoxData = Array<Doc>;
- }
-
export class GestureEvent {
constructor(
readonly gesture: Gestures,
readonly points: PointData[],
readonly bounds: Rect,
- readonly callbackFn?: Function
+ readonly callbackFn?: Function,
+ readonly text?: any
) { }
}
@@ -38,7 +36,8 @@ export namespace GestureUtils {
Box = "box",
Line = "line",
Stroke = "stroke",
- Scribble = "scribble"
+ Scribble = "scribble",
+ Text = "text"
}
export const GestureRecognizer = new NDollarRecognizer(false);
diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts
index 12c2b25bb..9e15ada2d 100644
--- a/src/pen-gestures/ndollar.ts
+++ b/src/pen-gestures/ndollar.ts
@@ -95,7 +95,7 @@ export class Unistroke {
constructor(public Name: string, useBoundedRotationInvariance: boolean, points: Point[]) {
this.Points = Resample(points, NumPoints);
- var radians = IndicativeAngle(this.Points);
+ const radians = IndicativeAngle(this.Points);
this.Points = RotateBy(this.Points, -radians);
this.Points = ScaleDimTo(this.Points, SquareSize, OneDThreshold);
if (useBoundedRotationInvariance) {
@@ -117,14 +117,14 @@ export class Multistroke {
{
this.NumStrokes = strokes.length; // number of individual strokes
- var order = new Array(strokes.length); // array of integer indices
+ const order = new Array(strokes.length); // array of integer indices
for (var i = 0; i < strokes.length; i++) {
order[i] = i; // initialize
}
- var orders = new Array(); // array of integer arrays
+ const orders = new Array(); // array of integer arrays
HeapPermute(strokes.length, order, /*out*/ orders);
- var unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays
+ const unistrokes = MakeUnistrokes(strokes, orders); // returns array of point arrays
this.Unistrokes = new Array(unistrokes.length); // unistrokes for this multistroke
for (var j = 0; j < unistrokes.length; j++) {
this.Unistrokes[j] = new Unistroke(this.Name, useBoundedRotationInvariance, unistrokes[j]);
@@ -168,7 +168,12 @@ export class NDollarRecognizer {
//
this.Multistrokes = new Array(NumMultistrokes);
this.Multistrokes[0] = new Multistroke(GestureUtils.Gestures.Box, useBoundedRotationInvariance, new Array(
- new Array(new Point(30, 146), new Point(30, 222), new Point(106, 225), new Point(106, 146), new Point(30, 146))
+ new Array(
+ new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200),
+ new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230),
+ new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160),
+ new Point(106, 146), //new Point(80, 150), new Point(50, 146),
+ new Point(30, 143))
));
this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(
new Array(new Point(12, 347), new Point(119, 347))
@@ -247,26 +252,26 @@ export class NDollarRecognizer {
}
Recognize = (strokes: any[], useBoundedRotationInvariance: boolean = false, requireSameNoOfStrokes: boolean = false, useProtractor: boolean = true) => {
- var t0 = Date.now();
- var points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
- var candidate = new Unistroke("", useBoundedRotationInvariance, points);
+ const t0 = Date.now();
+ const points = CombineStrokes(strokes); // make one connected unistroke from the given strokes
+ const candidate = new Unistroke("", useBoundedRotationInvariance, points);
var u = -1;
var b = +Infinity;
for (var i = 0; i < this.Multistrokes.length; i++) // for each multistroke template
{
- if (!requireSameNoOfStrokes || strokes.length == this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
+ if (!requireSameNoOfStrokes || strokes.length === this.Multistrokes[i].NumStrokes) // optional -- only attempt match when same # of component strokes
{
- for (var j = 0; j < this.Multistrokes[i].Unistrokes.length; j++) // for each unistroke within this multistroke
+ for (const unistroke of this.Multistrokes[i].Unistrokes) // for each unistroke within this multistroke
{
- if (AngleBetweenUnitVectors(candidate.StartUnitVector, this.Multistrokes[i].Unistrokes[j].StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
+ if (AngleBetweenUnitVectors(candidate.StartUnitVector, unistroke.StartUnitVector) <= AngleSimilarityThreshold) // strokes start in the same direction
{
var d;
if (useProtractor) {
- d = OptimalCosineDistance(this.Multistrokes[i].Unistrokes[j].Vector, candidate.Vector); // Protractor
+ d = OptimalCosineDistance(unistroke.Vector, candidate.Vector); // Protractor
}
else {
- d = DistanceAtBestAngle(candidate.Points, this.Multistrokes[i].Unistrokes[j], -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
+ d = DistanceAtBestAngle(candidate.Points, unistroke, -AngleRange, +AngleRange, AnglePrecision); // Golden Section Search (original $N)
}
if (d < b) {
b = d; // best (least) distance
@@ -276,15 +281,15 @@ export class NDollarRecognizer {
}
}
}
- var t1 = Date.now();
- return (u == -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
+ const t1 = Date.now();
+ return (u === -1) ? null : new Result(this.Multistrokes[u].Name, useProtractor ? (1.0 - b) : (1.0 - b / HalfDiagonal), t1 - t0);
}
AddGesture = (name: string, useBoundedRotationInvariance: boolean, strokes: any[]) => {
this.Multistrokes[this.Multistrokes.length] = new Multistroke(name, useBoundedRotationInvariance, strokes);
var num = 0;
- for (var i = 0; i < this.Multistrokes.length; i++) {
- if (this.Multistrokes[i].Name == name) {
+ for (const multistroke of this.Multistrokes) {
+ if (multistroke.Name === name) {
num++;
}
}
@@ -302,17 +307,17 @@ export class NDollarRecognizer {
// Private helper functions from here on down
//
function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
- if (n == 1) {
+ if (n === 1) {
orders[orders.length] = order.slice(); // append copy
} else {
for (var i = 0; i < n; i++) {
HeapPermute(n - 1, order, orders);
- if (n % 2 == 1) { // swap 0, n-1
- var tmp = order[0];
+ if (n % 2 === 1) { // swap 0, n-1
+ const tmp = order[0];
order[0] = order[n - 1];
order[n - 1] = tmp;
} else { // swap i, n-1
- var tmp = order[i];
+ const tmp = order[i];
order[i] = order[n - 1];
order[n - 1] = tmp;
}
@@ -321,21 +326,21 @@ function HeapPermute(n: number, order: any[], /*out*/ orders: any[]) {
}
function MakeUnistrokes(strokes: any, orders: any) {
- var unistrokes = new Array(); // array of point arrays
- for (var r = 0; r < orders.length; r++) {
- for (var b = 0; b < Math.pow(2, orders[r].length); b++) // use b's bits for directions
+ const unistrokes = new Array(); // array of point arrays
+ for (const order of orders) {
+ for (var b = 0; b < Math.pow(2, order.length); b++) // use b's bits for directions
{
- var unistroke = new Array(); // array of points
- for (var i = 0; i < orders[r].length; i++) {
+ const unistroke = new Array(); // array of points
+ for (var i = 0; i < order.length; i++) {
var pts;
- if (((b >> i) & 1) == 1) {// is b's bit at index i on?
- pts = strokes[orders[r][i]].slice().reverse(); // copy and reverse
+ if (((b >> i) & 1) === 1) {// is b's bit at index i on?
+ pts = strokes[order[i]].slice().reverse(); // copy and reverse
}
else {
- pts = strokes[orders[r][i]].slice(); // copy
+ pts = strokes[order[i]].slice(); // copy
}
- for (var p = 0; p < pts.length; p++) {
- unistroke[unistroke.length] = pts[p]; // append points
+ for (const point of pts) {
+ unistroke[unistroke.length] = point; // append points
}
}
unistrokes[unistrokes.length] = unistroke; // add one unistroke to set
@@ -345,69 +350,71 @@ function MakeUnistrokes(strokes: any, orders: any) {
}
function CombineStrokes(strokes: any) {
- var points = new Array();
- for (var s = 0; s < strokes.length; s++) {
- for (var p = 0; p < strokes[s].length; p++)
- points[points.length] = new Point(strokes[s][p].X, strokes[s][p].Y);
+ const points = new Array();
+ for (const stroke of strokes) {
+ for (const { X, Y } of stroke) {
+ points[points.length] = new Point(X, Y);
+ }
}
return points;
}
function Resample(points: any, n: any) {
- var I = PathLength(points) / (n - 1); // interval length
+ const I = PathLength(points) / (n - 1); // interval length
var D = 0.0;
- var newpoints = new Array(points[0]);
+ const newpoints = new Array(points[0]);
for (var i = 1; i < points.length; i++) {
- var d = Distance(points[i - 1], points[i]);
+ const d = Distance(points[i - 1], points[i]);
if ((D + d) >= I) {
- var qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
- var qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y);
- var q = new Point(qx, qy);
+ const qx = points[i - 1].X + ((I - D) / d) * (points[i].X - points[i - 1].X);
+ const qy = points[i - 1].Y + ((I - D) / d) * (points[i].Y - points[i - 1].Y);
+ const q = new Point(qx, qy);
newpoints[newpoints.length] = q; // append new point 'q'
points.splice(i, 0, q); // insert 'q' at position i in points s.t. 'q' will be the next i
D = 0.0;
}
else D += d;
}
- if (newpoints.length == n - 1) // somtimes we fall a rounding-error short of adding the last point, so add it if so
+ if (newpoints.length === n - 1) {// sometimes we fall a rounding-error short of adding the last point, so add it if so
newpoints[newpoints.length] = new Point(points[points.length - 1].X, points[points.length - 1].Y);
+ }
return newpoints;
}
function IndicativeAngle(points: any) {
- var c = Centroid(points);
+ const c = Centroid(points);
return Math.atan2(c.Y - points[0].Y, c.X - points[0].X);
}
function RotateBy(points: any, radians: any) // rotates points around centroid
{
- var c = Centroid(points);
- var cos = Math.cos(radians);
- var sin = Math.sin(radians);
- var newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- var qx = (points[i].X - c.X) * cos - (points[i].Y - c.Y) * sin + c.X
- var qy = (points[i].X - c.X) * sin + (points[i].Y - c.Y) * cos + c.Y;
+ const c = Centroid(points);
+ const cos = Math.cos(radians);
+ const sin = Math.sin(radians);
+ const newpoints = new Array();
+ for (const point of points) {
+ const qx = (point.X - c.X) * cos - (point.Y - c.Y) * sin + c.X;
+ const qy = (point.X - c.X) * sin + (point.Y - c.Y) * cos + c.Y;
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
}
function ScaleDimTo(points: any, size: any, ratio1D: any) // scales bbox uniformly for 1D, non-uniformly for 2D
{
- var B = BoundingBox(points);
- var uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
- var newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- var qx = uniformly ? points[i].X * (size / Math.max(B.Width, B.Height)) : points[i].X * (size / B.Width);
- var qy = uniformly ? points[i].Y * (size / Math.max(B.Width, B.Height)) : points[i].Y * (size / B.Height);
+ const B = BoundingBox(points);
+ const uniformly = Math.min(B.Width / B.Height, B.Height / B.Width) <= ratio1D; // 1D or 2D gesture test
+ const newpoints = new Array();
+ for (const { X, Y } of points) {
+ const qx = uniformly ? X * (size / Math.max(B.Width, B.Height)) : X * (size / B.Width);
+ const qy = uniformly ? Y * (size / Math.max(B.Width, B.Height)) : Y * (size / B.Height);
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
}
function TranslateTo(points: any, pt: any) // translates points' centroid
{
- var c = Centroid(points);
- var newpoints = new Array();
- for (var i = 0; i < points.length; i++) {
- var qx = points[i].X + pt.X - c.X;
- var qy = points[i].Y + pt.Y - c.Y;
+ const c = Centroid(points);
+ const newpoints = new Array();
+ for (const { X, Y } of points) {
+ const qx = X + pt.X - c.X;
+ const qy = Y + pt.Y - c.Y;
newpoints[newpoints.length] = new Point(qx, qy);
}
return newpoints;
@@ -417,21 +424,21 @@ function Vectorize(points: any, useBoundedRotationInvariance: any) // for Protra
var cos = 1.0;
var sin = 0.0;
if (useBoundedRotationInvariance) {
- var iAngle = Math.atan2(points[0].Y, points[0].X);
- var baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0));
+ const iAngle = Math.atan2(points[0].Y, points[0].X);
+ const baseOrientation = (Math.PI / 4.0) * Math.floor((iAngle + Math.PI / 8.0) / (Math.PI / 4.0));
cos = Math.cos(baseOrientation - iAngle);
sin = Math.sin(baseOrientation - iAngle);
}
var sum = 0.0;
- var vector = new Array<number>();
+ const vector = new Array<number>();
for (var i = 0; i < points.length; i++) {
- var newX = points[i].X * cos - points[i].Y * sin;
- var newY = points[i].Y * cos + points[i].X * sin;
+ const newX = points[i].X * cos - points[i].Y * sin;
+ const newY = points[i].Y * cos + points[i].X * sin;
vector[vector.length] = newX;
vector[vector.length] = newY;
sum += newX * newX + newY * newY;
}
- var magnitude = Math.sqrt(sum);
+ const magnitude = Math.sqrt(sum);
for (var i = 0; i < vector.length; i++) {
vector[i] /= magnitude;
}
@@ -445,7 +452,7 @@ function OptimalCosineDistance(v1: any, v2: any) // for Protractor
a += v1[i] * v2[i] + v1[i + 1] * v2[i + 1];
b += v1[i] * v2[i + 1] - v1[i + 1] * v2[i];
}
- var angle = Math.atan(b / a);
+ const angle = Math.atan(b / a);
return Math.acos(a * Math.cos(angle) + b * Math.sin(angle));
}
function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any) {
@@ -471,14 +478,14 @@ function DistanceAtBestAngle(points: any, T: any, a: any, b: any, threshold: any
return Math.min(f1, f2);
}
function DistanceAtAngle(points: any, T: any, radians: any) {
- var newpoints = RotateBy(points, radians);
+ const newpoints = RotateBy(points, radians);
return PathDistance(newpoints, T.Points);
}
function Centroid(points: any) {
var x = 0.0, y = 0.0;
- for (var i = 0; i < points.length; i++) {
- x += points[i].X;
- y += points[i].Y;
+ for (const point of points) {
+ x += point.X;
+ y += point.Y;
}
x /= points.length;
y /= points.length;
@@ -486,44 +493,46 @@ function Centroid(points: any) {
}
function BoundingBox(points: any) {
var minX = +Infinity, maxX = -Infinity, minY = +Infinity, maxY = -Infinity;
- for (var i = 0; i < points.length; i++) {
- minX = Math.min(minX, points[i].X);
- minY = Math.min(minY, points[i].Y);
- maxX = Math.max(maxX, points[i].X);
- maxY = Math.max(maxY, points[i].Y);
+ for (const { X, Y } of points) {
+ minX = Math.min(minX, X);
+ minY = Math.min(minY, Y);
+ maxX = Math.max(maxX, X);
+ maxY = Math.max(maxY, Y);
}
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
function PathDistance(pts1: any, pts2: any) // average distance between corresponding points in two paths
{
var d = 0.0;
- for (var i = 0; i < pts1.length; i++) // assumes pts1.length == pts2.length
+ for (var i = 0; i < pts1.length; i++) {// assumes pts1.length == pts2.length
d += Distance(pts1[i], pts2[i]);
+ }
return d / pts1.length;
}
function PathLength(points: any) // length traversed by a point path
{
var d = 0.0;
- for (var i = 1; i < points.length; i++)
+ for (var i = 1; i < points.length; i++) {
d += Distance(points[i - 1], points[i]);
+ }
return d;
}
function Distance(p1: any, p2: any) // distance between two points
{
- var dx = p2.X - p1.X;
- var dy = p2.Y - p1.Y;
+ const dx = p2.X - p1.X;
+ const dy = p2.Y - p1.Y;
return Math.sqrt(dx * dx + dy * dy);
}
function CalcStartUnitVector(points: any, index: any) // start angle from points[0] to points[index] normalized as a unit vector
{
- var v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
- var len = Math.sqrt(v.X * v.X + v.Y * v.Y);
+ const v = new Point(points[index].X - points[0].X, points[index].Y - points[0].Y);
+ const len = Math.sqrt(v.X * v.X + v.Y * v.Y);
return new Point(v.X / len, v.Y / len);
}
function AngleBetweenUnitVectors(v1: any, v2: any) // gives acute angle between unit vectors from (0,0) to v1, and (0,0) to v2
{
- var n = (v1.X * v2.X + v1.Y * v2.Y);
- var c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1]
+ const n = (v1.X * v2.X + v1.Y * v2.Y);
+ const c = Math.max(-1.0, Math.min(1.0, n)); // ensure [-1,+1]
return Math.acos(c); // arc cosine of the vector dot product
}
function Deg2Rad(d: any) { return (d * Math.PI / 180.0); } \ No newline at end of file
diff --git a/src/scraping/buxton/node_scraper.ts b/src/scraping/buxton/node_scraper.ts
new file mode 100644
index 000000000..ef1d989d4
--- /dev/null
+++ b/src/scraping/buxton/node_scraper.ts
@@ -0,0 +1,57 @@
+import { readdirSync } from "fs";
+import { resolve } from "path";
+
+const StreamZip = require('node-stream-zip');
+
+export async function open(path: string) {
+ const zip = new StreamZip({
+ file: path,
+ storeEntries: true
+ });
+ return new Promise<string>((resolve, reject) => {
+ zip.on('ready', () => {
+ console.log("READY!", zip.entriesCount);
+ for (const entry of Object.values(zip.entries()) as any[]) {
+ const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
+ console.log(`Entry ${entry.name}: ${desc}`);
+ }
+ let body = "";
+ zip.stream("word/document.xml", (error: any, stream: any) => {
+ if (error) {
+ reject(error);
+ }
+ stream.on('data', (chunk: any) => body += chunk.toString());
+ stream.on('end', () => {
+ resolve(body);
+ zip.close();
+ });
+ });
+ });
+ });
+}
+
+export async function extract(path: string) {
+ const contents = await open(path);
+ let body = "";
+ const components = contents.toString().split('<w:t');
+ for (const component of components) {
+ const tags = component.split('>');
+ console.log(tags[1]);
+ const content = tags[1].replace(/<.*$/, "");
+ body += content;
+ }
+ return body;
+}
+
+async function parse(): Promise<string[]> {
+ const sourceDirectory = resolve(`${__dirname}/source`);
+ const candidates = readdirSync(sourceDirectory).filter(file => file.endsWith(".doc") || file.endsWith(".docx")).map(file => `${sourceDirectory}/${file}`);
+ await extract(candidates[0]);
+ try {
+ return Promise.all(candidates.map(extract));
+ } catch {
+ return [];
+ }
+}
+
+parse(); \ No newline at end of file
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index a9256073b..ec9c3f72c 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -13,11 +13,12 @@ import math
import sys
source = "./source"
-dist = "../../server/public/files"
+filesPath = "../../server/public/files"
+image_dist = filesPath + "/images/buxton"
db = MongoClient("localhost", 27017)["Dash"]
target_collection = db.newDocuments
-target_doc_title = "Workspace 1"
+target_doc_title = "Collection 1"
schema_guids = []
common_proto_id = ""
@@ -70,7 +71,7 @@ def text_doc_map(string_list):
return listify(proxify_guids(list(map(guid_map, string_list))))
-def write_collection(parse_results, display_fields, storage_key, viewType=2):
+def write_collection(parse_results, display_fields, storage_key, viewType):
view_guids = parse_results["child_guids"]
data_doc = parse_results["schema"]
@@ -84,13 +85,14 @@ def write_collection(parse_results, display_fields, storage_key, viewType=2):
"proto": protofy(data_doc["_id"]),
"x": 10,
"y": 10,
- "width": 900,
- "height": 600,
- "panX": 0,
- "panY": 0,
+ "_width": 900,
+ "_height": 600,
+ "_panX": 0,
+ "_panY": 0,
"zIndex": 2,
"libraryBrush": False,
- "viewType": viewType
+ "_viewType": viewType,
+ "_LODdisable": True
},
"__type": "Doc"
}
@@ -98,16 +100,17 @@ def write_collection(parse_results, display_fields, storage_key, viewType=2):
fields["proto"] = protofy(common_proto_id)
fields[storage_key] = listify(proxify_guids(view_guids))
fields["schemaColumns"] = listify(display_fields)
- fields["backgroundColor"] = "white"
- fields["scale"] = 0.5
- fields["viewType"] = 2
fields["author"] = "Bill Buxton"
fields["creationDate"] = {
"date": datetime.datetime.utcnow().microsecond,
"__type": "date"
}
+ if "image_urls" in parse_results:
+ fields["hero"] = {
+ "url": parse_results["image_urls"][0],
+ "__type": "image"
+ }
fields["isPrototype"] = True
- fields["page"] = -1
target_collection.insert_one(data_doc)
target_collection.insert_one(view_doc)
@@ -129,7 +132,7 @@ def write_text_doc(content):
"proto": protofy(data_doc_guid),
"x": 10,
"y": 10,
- "width": 400,
+ "_width": 400,
"zIndex": 2
},
"__type": "Doc"
@@ -144,17 +147,17 @@ def write_text_doc(content):
"__type": "RichTextField"
},
"title": content,
- "nativeWidth": 200,
+ "_nativeWidth": 200,
"author": "Bill Buxton",
"creationDate": {
"date": datetime.datetime.utcnow().microsecond,
"__type": "date"
},
"isPrototype": True,
- "autoHeight": True,
+ "_autoHeight": True,
"page": -1,
- "nativeHeight": 200,
- "height": 200,
+ "_nativeHeight": 200,
+ "_height": 200,
"data_text": content
},
"__type": "Doc"
@@ -167,22 +170,27 @@ def write_text_doc(content):
def write_image(folder, name):
- path = f"http://localhost:1050/files/{folder}/{name}"
+ path = f"http://localhost:1050/files/images/buxton/{folder}/{name}"
data_doc_guid = guid()
view_doc_guid = guid()
- image = Image.open(f"{dist}/{folder}/{name}")
+ image = Image.open(f"{image_dist}/{folder}/{name}")
native_width, native_height = image.size
+ if abs(native_width - native_height) < 10:
+ return None
+
view_doc = {
"_id": view_doc_guid,
"fields": {
"proto": protofy(data_doc_guid),
"x": 10,
"y": 10,
- "width": min(800, native_width),
- "zIndex": 2
+ "_width": min(800, native_width),
+ "zIndex": 2,
+ "widthUnit": "*",
+ "widthMagnitude": 1
},
"__type": "Doc"
}
@@ -196,7 +204,7 @@ def write_image(folder, name):
"__type": "image"
},
"title": name,
- "nativeWidth": native_width,
+ "_nativeWidth": native_width,
"author": "Bill Buxton",
"creationDate": {
"date": datetime.datetime.utcnow().microsecond,
@@ -204,8 +212,8 @@ def write_image(folder, name):
},
"isPrototype": True,
"page": -1,
- "nativeHeight": native_height,
- "height": native_height
+ "_nativeHeight": native_height,
+ "_height": native_height
},
"__type": "Doc"
}
@@ -213,7 +221,10 @@ def write_image(folder, name):
target_collection.insert_one(view_doc)
target_collection.insert_one(data_doc)
- return view_doc_guid
+ return {
+ "layout_id": view_doc_guid,
+ "url": path
+ }
def parse_document(file_name: str):
@@ -222,20 +233,26 @@ def parse_document(file_name: str):
result = {}
- dir_path = dist + "/" + pure_name
+ dir_path = image_dist + "/" + pure_name
+ print(dir_path)
mkdir_if_absent(dir_path)
raw = str(docx2txt.process(source + "/" + file_name, dir_path))
+ urls = []
view_guids = []
count = 0
for image in os.listdir(dir_path):
- count += 1
- view_guids.append(write_image(pure_name, image))
- copyfile(dir_path + "/" + image, dir_path +
- "/" + image.replace(".", "_o.", 1))
- copyfile(dir_path + "/" + image, dir_path +
- "/" + image.replace(".", "_m.", 1))
+ created = write_image(pure_name, image)
+ if created != None:
+ urls.append(created["url"])
+ view_guids.append(created["layout_id"])
+ count += 1
+ resolved = dir_path + "/" + image
+ original = dir_path + "/" + image.replace(".", "_o.", 1)
+ medium = dir_path + "/" + image.replace(".", "_m.", 1)
+ copyfile(resolved, original)
+ copyfile(resolved, medium)
print(f"extracted {count} images...")
def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace(
@@ -342,12 +359,13 @@ def parse_document(file_name: str):
"fields": result,
"__type": "Doc"
},
- "child_guids": view_guids
+ "child_guids": view_guids,
+ "image_urls": urls
}
def proxify_guids(guids):
- return list(map(lambda guid: {"fieldId": guid, "__type": "proxy"}, guids))
+ return list(map(lambda guid: {"fieldId": guid, "__type": "prefetch_proxy"}, guids))
def write_common_proto():
@@ -356,21 +374,19 @@ def write_common_proto():
"_id": id,
"fields": {
"proto": protofy("collectionProto"),
- "title": "Common Import Proto",
+ "title": "The Buxton Collection",
},
"__type": "Doc"
}
-
target_collection.insert_one(common_proto)
-
return id
-if os.path.exists(dist):
- shutil.rmtree(dist)
-while os.path.exists(dist):
+if os.path.exists(image_dist):
+ shutil.rmtree(image_dist)
+while os.path.exists(image_dist):
pass
-os.mkdir(dist)
+os.mkdir(image_dist)
mkdir_if_absent(source)
common_proto_id = write_common_proto()
@@ -380,7 +396,7 @@ for file_name in os.listdir(source):
if file_name.endswith('.docx'):
candidates += 1
schema_guids.append(write_collection(
- parse_document(file_name), ["title", "data"], "image_data"))
+ parse_document(file_name), ["title", "data"], "data", 5))
print("writing parent schema...")
parent_guid = write_collection({
@@ -390,7 +406,7 @@ parent_guid = write_collection({
"__type": "Doc"
},
"child_guids": schema_guids
-}, ["title", "short_description", "original_price"], "data", 1)
+}, ["title", "short_description", "original_price"], "data", 2)
print("appending parent schema to main workspace...\n")
target_collection.update_one(
@@ -400,7 +416,7 @@ target_collection.update_one(
print("rewriting .gitignore...\n")
lines = ['*', '!.gitignore']
-with open(dist + "/.gitignore", 'w') as f:
+with open(filesPath + "/.gitignore", 'w') as f:
f.write('\n'.join(lines))
suffix = "" if candidates == 1 else "s"
diff --git a/src/scraping/buxton/source/Bill_Notes_NB75D.docx b/src/scraping/buxton/source/Bill_Notes_NB75D.docx
new file mode 100644
index 000000000..a5a5e3d90
--- /dev/null
+++ b/src/scraping/buxton/source/Bill_Notes_NB75D.docx
Binary files differ
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
index a93566fb1..60f66c878 100644
--- a/src/server/ActionUtilities.ts
+++ b/src/server/ActionUtilities.ts
@@ -6,6 +6,7 @@ import * as rimraf from "rimraf";
import { yellow, Color } from 'colors';
import * as nodemailer from "nodemailer";
import { MailOptions } from "nodemailer/lib/json-transport";
+import Mail = require('nodemailer/lib/mailer');
const projectRoot = path.resolve(__dirname, "../../");
export function pathFromRoot(relative?: string) {
@@ -118,16 +119,24 @@ export namespace Email {
}
});
+ export interface DispatchOptions<T extends string | string[]> {
+ to: T;
+ subject: string;
+ content: string;
+ attachments?: Mail.Attachment | Mail.Attachment[];
+ }
+
export interface DispatchFailure {
recipient: string;
error: Error;
}
- export async function dispatchAll(recipients: string[], subject: string, content: string) {
+ export async function dispatchAll({ to, subject, content, attachments }: DispatchOptions<string[]>) {
const failures: DispatchFailure[] = [];
- await Promise.all(recipients.map(async (recipient: string) => {
+ await Promise.all(to.map(async recipient => {
let error: Error | null;
- if ((error = await Email.dispatch(recipient, subject, content)) !== null) {
+ const resolved = attachments ? "length" in attachments ? attachments : [attachments] : undefined;
+ if ((error = await Email.dispatch({ to: recipient, subject, content, attachments: resolved })) !== null) {
failures.push({
recipient,
error
@@ -137,16 +146,15 @@ export namespace Email {
return failures.length ? failures : undefined;
}
- export async function dispatch(recipient: string, subject: string, content: string): Promise<Error | null> {
+ export async function dispatch({ to, subject, content, attachments }: DispatchOptions<string>): Promise<Error | null> {
const mailOptions = {
- to: recipient,
+ to,
from: 'brownptcdash@gmail.com',
subject,
- text: `Hello ${recipient.split("@")[0]},\n\n${content}`
+ text: `Hello ${to.split("@")[0]},\n\n${content}`,
+ attachments
} as MailOptions;
- return new Promise<Error | null>(resolve => {
- smtpTransport.sendMail(mailOptions, resolve);
- });
+ return new Promise<Error | null>(resolve => smtpTransport.sendMail(mailOptions, resolve));
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index 88dfa6a64..be452c0ff 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,5 +1,5 @@
import ApiManager, { Registration } from "./ApiManager";
-import { Method, _permission_denied } from "../RouteManager";
+import { Method, _permission_denied, PublicHandler } from "../RouteManager";
import { WebSocket } from "../Websocket/Websocket";
import { Database } from "../database";
@@ -31,6 +31,21 @@ export default class DeleteManager extends ApiManager {
}
});
+ const hi: PublicHandler = async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Instance.deleteAll('users');
+ res.redirect("/home");
+ };
+
+ // register({
+ // method: Method.GET,
+ // subscription: "/deleteUsers",
+ // onValidation: hi,
+ // onUnauthenticated: hi
+ // });
+
register({
method: Method.GET,
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 316ba09ed..4ce12f9f3 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -8,6 +8,7 @@ import { red, cyan, yellow } from "colors";
import RouteSubscriber from "../RouteSubscriber";
import { exec } from "child_process";
import { onWindows } from "..";
+import { get } from "request-promise";
export class SearchManager extends ApiManager {
@@ -68,18 +69,25 @@ export class SearchManager extends ApiManager {
export namespace SolrManager {
+ const command = onWindows ? "solr.cmd" : "solr";
+
export async function SetRunning(status: boolean): Promise<boolean> {
const args = status ? "start" : "stop -p 8983";
- console.log(`Solr management: trying to ${args}`);
- exec(`${onWindows ? "solr.cmd" : "solr"} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
+ console.log(`solr management: trying to ${args}`);
+ exec(`${command} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
if (error) {
+ console.log(red(`solr management error: unable to ${args} server`));
console.log(red(error.message));
- console.log(red(`Solr management error: unable to ${args}`));
}
console.log(cyan(stdout));
console.log(yellow(stderr));
});
- return true;
+ try {
+ await get("http://localhost:8983");
+ return true;
+ } catch {
+ return false;
+ }
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
new file mode 100644
index 000000000..f1629b8f0
--- /dev/null
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -0,0 +1,58 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied, AuthorizedCore, SecureHandler } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import { sessionAgent } from "..";
+import { DashSessionAgent } from "../DashSession/DashSessionAgent";
+
+const permissionError = "You are not authorized!";
+
+export default class SessionManager extends ApiManager {
+
+ private secureSubscriber = (root: string, ...params: string[]) => new RouteSubscriber(root).add("session_key", ...params);
+
+ private authorizedAction = (handler: SecureHandler) => {
+ return (core: AuthorizedCore) => {
+ const { req: { params }, res, isRelease } = core;
+ if (!isRelease) {
+ return res.send("This can be run only on the release server.");
+ }
+ if (params.session_key !== process.env.session_key) {
+ return _permission_denied(res, permissionError);
+ }
+ return handler(core);
+ };
+ }
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("debug", "to?"),
+ secureHandler: this.authorizedAction(async ({ req: { params }, res }) => {
+ const to = params.to || DashSessionAgent.notificationRecipient;
+ const { error } = await sessionAgent.serverWorker.emit("debug", { to });
+ res.send(error ? error.message : `Your request was successful: the server captured and compressed (but did not save) a new back up. It was sent to ${to}.`);
+ })
+ });
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("backup"),
+ secureHandler: this.authorizedAction(async ({ res }) => {
+ const { error } = await sessionAgent.serverWorker.emit("backup");
+ res.send(error ? error.message : "Your request was successful: the server successfully created a new back up.");
+ })
+ });
+
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("kill"),
+ secureHandler: this.authorizedAction(({ res }) => {
+ res.send("Your request was successful: the server and its session have been killed.");
+ sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route");
+ })
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 74f45ae62..a92b613b7 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -40,7 +40,7 @@ export default class UploadManager extends ApiManager {
register({
method: Method.POST,
- subscription: "/upload",
+ subscription: "/uploadFormData",
secureHandler: async ({ req, res }) => {
const form = new formidable.IncomingForm();
form.uploadDir = pathToDirectory(Directory.parsed_files);
@@ -61,6 +61,18 @@ export default class UploadManager extends ApiManager {
register({
method: Method.POST,
+ subscription: "/uploadRemoteImage",
+ secureHandler: async ({ req, res }) => {
+ const { sources } = req.body;
+ if (Array.isArray(sources)) {
+ return res.send(await Promise.all(sources.map(url => DashUploadUtils.UploadImage(url))));
+ }
+ res.send();
+ }
+ });
+
+ register({
+ method: Method.POST,
subscription: "/uploadDoc",
secureHandler: ({ req, res }) => {
const form = new formidable.IncomingForm();
@@ -169,8 +181,7 @@ export default class UploadManager extends ApiManager {
secureHandler: async ({ req, res }) => {
const { source } = req.body;
if (typeof source === "string") {
- const { serverAccessPaths } = await DashUploadUtils.UploadImage(source);
- return res.send(await DashUploadUtils.InspectImage(serverAccessPaths[SizeSuffix.Original]));
+ return res.send(await DashUploadUtils.InspectImage(source));
}
res.send({});
}
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index f2ef22961..b0d868918 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -2,6 +2,8 @@ import ApiManager, { Registration } from "./ApiManager";
import { Method } from "../RouteManager";
import { Database } from "../database";
import { msToTime } from "../ActionUtilities";
+import * as bcrypt from "bcrypt-nodejs";
+import { Opt } from "../../new_fields/Doc";
export const timeMap: { [id: string]: number } = {};
interface ActivityUnit {
@@ -37,6 +39,59 @@ export default class UserManager extends ApiManager {
});
register({
+ method: Method.POST,
+ subscription: '/internalResetPassword',
+ secureHandler: async ({ user, req, res }) => {
+ const result: any = {};
+ const { curr_pass, new_pass, new_confirm } = req.body;
+ // perhaps should assert whether curr password is entered correctly
+ const validated = await new Promise<Opt<boolean>>(resolve => {
+ bcrypt.compare(curr_pass, user.password, (err, passwords_match) => {
+ if (err || !passwords_match) {
+ result.error = [{ msg: "Incorrect current password" }];
+ res.send(result);
+ resolve(undefined);
+ } else {
+ resolve(passwords_match);
+ }
+ });
+ });
+
+ if (validated === undefined) {
+ return;
+ }
+
+ req.assert("new_pass", "Password must be at least 4 characters long").len({ min: 4 });
+ req.assert("new_confirm", "Passwords do not match").equals(new_pass);
+ if (curr_pass === new_pass) {
+ result.error = [{ msg: "Current and new password are the same" }];
+ }
+ // was there error in validating new passwords?
+ if (req.validationErrors()) {
+ // was there error?
+ result.error = req.validationErrors();
+ }
+
+ // will only change password if there are no errors.
+ if (!result.error) {
+ user.password = new_pass;
+ user.passwordResetToken = undefined;
+ user.passwordResetExpires = undefined;
+ }
+
+ user.save(err => {
+ if (err) {
+ result.error = [{ msg: "Error while saving new password" }];
+ }
+ });
+
+ res.send(result);
+ }
+ });
+
+
+
+ register({
method: Method.GET,
subscription: "/activity",
secureHandler: ({ res }) => {
diff --git a/src/server/DashSession.ts b/src/server/DashSession.ts
deleted file mode 100644
index 83ce7caaf..000000000
--- a/src/server/DashSession.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Session } from "./Session/session";
-import { Email } from "./ActionUtilities";
-import { red, yellow } from "colors";
-import { SolrManager } from "./ApiManagers/SearchManager";
-import { execSync } from "child_process";
-import { Utils } from "../Utils";
-import { WebSocket } from "./Websocket/Websocket";
-import { MessageStore } from "./Message";
-import { launchServer } from ".";
-
-/**
-* If we're the monitor (master) thread, we should launch the monitor logic for the session.
-* Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
-* our job should be to run the server.
-*/
-export class DashSessionAgent extends Session.AppliedSessionAgent {
-
- private readonly notificationRecipients = ["samuel_wilkins@brown.edu"];
- private readonly signature = "-Dash Server Session Manager";
-
- protected async launchMonitor() {
- const monitor = Session.Monitor.Create({
- key: async key => {
- // this sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
- // to kill the server via the /kill/:key route
- const content = `The key for this session (started @ ${new Date().toUTCString()}) is ${key}.\n\n${this.signature}`;
- const failures = await Email.dispatchAll(this.notificationRecipients, "Server Termination Key", content);
- if (failures) {
- failures.map(({ recipient, error: { message } }) => monitor.log(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- },
- crash: async ({ name, message, stack }) => {
- const body = [
- "You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:",
- `name:\n${name}`,
- `message:\n${message}`,
- `stack:\n${stack}`,
- "The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.",
- ].join("\n\n");
- const content = `${body}\n\n${this.signature}`;
- const failures = await Email.dispatchAll(this.notificationRecipients, "Dash Web Server Crash", content);
- if (failures) {
- failures.map(({ recipient, error: { message } }) => monitor.log(red(`dispatch failure @ ${recipient} (${yellow(message)})`)));
- return false;
- }
- return true;
- }
- });
- monitor.addReplCommand("pull", [], () => execSync("git pull", { stdio: ["ignore", "inherit", "inherit"] }));
- monitor.addReplCommand("solr", [/start|stop/], args => SolrManager.SetRunning(args[0] === "start"));
- return monitor;
- }
-
- protected async launchServerWorker() {
- const worker = Session.ServerWorker.Create(launchServer); // server initialization delegated to worker
- worker.addExitHandler(() => Utils.Emit(WebSocket._socket, MessageStore.ConnectionTerminated, "Manual"));
- return worker;
- }
-
-} \ No newline at end of file
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
new file mode 100644
index 000000000..c55e01243
--- /dev/null
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -0,0 +1,223 @@
+import { Email, pathFromRoot } from "../ActionUtilities";
+import { red, yellow, green, cyan } from "colors";
+import { get } from "request-promise";
+import { Utils } from "../../Utils";
+import { WebSocket } from "../Websocket/Websocket";
+import { MessageStore } from "../Message";
+import { launchServer, onWindows } from "..";
+import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
+import * as Archiver from "archiver";
+import { resolve } from "path";
+import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session";
+import rimraf = require("rimraf");
+
+/**
+ * If we're the monitor (master) thread, we should launch the monitor logic for the session.
+ * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
+ * our job should be to run the server.
+ */
+export class DashSessionAgent extends AppliedSessionAgent {
+
+ private readonly signature = "-Dash Server Session Manager";
+ private readonly releaseDesktop = pathFromRoot("../../Desktop");
+
+ /**
+ * The core method invoked when the single master thread is initialized.
+ * Installs event hooks, repl commands and additional IPC listeners.
+ */
+ protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise<void> {
+ await this.dispatchSessionPassword(sessionKey);
+ monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
+ monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
+ monitor.addReplCommand("backup", [], this.backup);
+ monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to));
+ monitor.on("backup", this.backup);
+ monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to));
+ monitor.coreHooks.onCrashDetected(this.dispatchCrashReport);
+ }
+
+ /**
+ * The core method invoked when a server worker thread is initialized.
+ * Installs logic to be executed when the server worker dies.
+ */
+ protected async initializeServerWorker(): Promise<ServerWorker> {
+ const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
+ worker.addExitHandler(this.notifyClient);
+ return worker;
+ }
+
+ /**
+ * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
+ */
+ private _remoteDebugInstructions: string | undefined;
+ private generateDebugInstructions = (zipName: string, target: string): string => {
+ if (!this._remoteDebugInstructions) {
+ this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._remoteDebugInstructions
+ .replace(/__zipname__/, zipName)
+ .replace(/__target__/, target)
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * Prepares the body of the email with information regarding a crash event.
+ */
+ private _crashInstructions: string | undefined;
+ private generateCrashInstructions({ name, message, stack }: Error): string {
+ if (!this._crashInstructions) {
+ this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._crashInstructions
+ .replace(/__name__/, name || "[no error name found]")
+ .replace(/__message__/, message || "[no error message found]")
+ .replace(/__stack__/, stack || "[no error stack found]")
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
+ * to kill the server via the /kill/:key route.
+ */
+ private dispatchSessionPassword = async (sessionKey: string): Promise<void> => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ mainLog(green("dispatching session key..."));
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Release Session Admin Authentication Key",
+ content: [
+ `Here's the key for this session (started @ ${new Date().toUTCString()}):`,
+ sessionKey,
+ this.signature
+ ].join("\n\n")
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ mainLog(red("distribution of session key experienced errors"));
+ } else {
+ mainLog(green("successfully distributed session key to recipients"));
+ }
+ }
+
+ /**
+ * This sends an email with the generated crash report.
+ */
+ private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Web Server Crash",
+ content: this.generateCrashInstructions(crashCause)
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`));
+ mainLog(red("distribution of crash notification experienced errors"));
+ } else {
+ mainLog(green("successfully distributed crash notification to recipients"));
+ }
+ }
+
+ /**
+ * Logic for interfacing with Solr. Either starts it,
+ * stops it, or rebuilds its indicies.
+ */
+ private executeSolrCommand = async (args: string[]): Promise<void> => {
+ const { exec, mainLog } = this.sessionMonitor;
+ const action = args[0];
+ if (action === "index") {
+ exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
+ } else {
+ const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
+ await exec(command, { cwd: "./solr-8.3.1/bin" });
+ try {
+ await get("http://localhost:8983");
+ mainLog(green("successfully connected to 8983 after running solr initialization"));
+ } catch {
+ mainLog(red("unable to connect at 8983 after running solr initialization"));
+ }
+ }
+ }
+
+ /**
+ * Broadcast to all clients that their connection
+ * is no longer valid, and explain why / what to expect.
+ */
+ private notifyClient: ExitHandler = reason => {
+ const { _socket } = WebSocket;
+ if (_socket) {
+ const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
+ Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+ }
+ }
+
+ /**
+ * Performs a backup of the database, saved to the desktop subdirectory.
+ * This should work as is only on our specific release server.
+ */
+ private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+
+ /**
+ * Compress either a brand new backup or the most recent backup and send it
+ * as an attachment to an email, dispatched to the requested recipient.
+ * @param mode specifies whether or not to make a new backup before exporting
+ * @param to the recipient of the email
+ */
+ private async dispatchZippedDebugBackup(to: string): Promise<void> {
+ const { mainLog } = this.sessionMonitor;
+ try {
+ // if desired, complete an immediate backup to send
+ await this.backup();
+ mainLog("backup complete");
+
+ const backupsDirectory = `${this.releaseDesktop}/backups`;
+
+ // sort all backups by their modified time, and choose the most recent one
+ const target = readdirSync(backupsDirectory).map(filename => ({
+ modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
+ filename
+ })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
+ mainLog(`targeting ${target}...`);
+
+ // create a zip file and to it, write the contents of the backup directory
+ const zipName = `${target}.zip`;
+ const zipPath = `${this.releaseDesktop}/${zipName}`;
+ const targetPath = `${backupsDirectory}/${target}`;
+ const output = createWriteStream(zipPath);
+ const zip = Archiver('zip');
+ zip.pipe(output);
+ zip.directory(`${targetPath}/Dash`, false);
+ await zip.finalize();
+ mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
+
+ // dispatch the email to the recipient, containing the finalized zip file
+ const error = await Email.dispatch({
+ to,
+ subject: `Remote debug: compressed backup of ${target}...`,
+ content: this.generateDebugInstructions(zipName, target),
+ attachments: [{ filename: zipName, path: zipPath }]
+ });
+
+ // since this is intended to be a zero-footprint operation, clean up
+ // by unlinking both the backup generated earlier in the function and the compressed zip file.
+ // to generate a persistent backup, just run backup.
+ unlinkSync(zipPath);
+ rimraf.sync(targetPath);
+
+ // indicate success or failure
+ mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
+ error && mainLog(red(error.message));
+ } catch (error) {
+ mainLog(red("unable to dispatch zipped backup..."));
+ mainLog(red(error.message));
+ }
+ }
+
+}
+
+export namespace DashSessionAgent {
+
+ export const notificationRecipient = "brownptcdash@gmail.com";
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/templates/crash_instructions.txt b/src/server/DashSession/templates/crash_instructions.txt
new file mode 100644
index 000000000..65417919d
--- /dev/null
+++ b/src/server/DashSession/templates/crash_instructions.txt
@@ -0,0 +1,14 @@
+You, as a Dash Administrator, are being notified of a server crash event. Here's what we know:
+
+name:
+__name__
+
+message:
+__message__
+
+stack:
+__stack__
+
+The server is already restarting itself, but if you're concerned, use the Remote Desktop Connection to monitor progress.
+
+__signature__ \ No newline at end of file
diff --git a/src/server/DashSession/templates/remote_debug_instructions.txt b/src/server/DashSession/templates/remote_debug_instructions.txt
new file mode 100644
index 000000000..c279c460a
--- /dev/null
+++ b/src/server/DashSession/templates/remote_debug_instructions.txt
@@ -0,0 +1,16 @@
+Instructions:
+
+Download this attachment, open your downloads folder and find this file (__zipname__).
+Right click on the zip file and select 'Extract to __target__\'.
+Open up the command line, and remember that you can get the path to any file or directory by literally dragging it from the file system and dropping it onto the terminal.
+Unless it's in your path, you'll want to navigate to the MongoDB bin directory, given for Windows:
+
+cd '/c/Program Files/MongoDB/Server/[your version, i.e. 4.0, goes here]/bin'
+
+Then run the following command (if you're in the bin folder, make that ./mongorestore ...):
+
+mongorestore --gzip [/path/to/directory/you/just/unzipped] --db Dash
+
+Assuming everything runs well, this will mirror your local database with that of the server. Now, just start the server locally and debug.
+
+__signature__ \ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index d9d985ca5..cb7104757 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,4 +1,4 @@
-import * as fs from 'fs';
+import { unlinkSync, createWriteStream, readFileSync, rename } from 'fs';
import { Utils } from '../Utils';
import * as path from 'path';
import * as sharp from 'sharp';
@@ -12,8 +12,9 @@ import { basename } from "path";
import { createIfNotExists } from './ActionUtilities';
import { ParsedPDF } from "../server/PdfTypes";
const parse = require('pdf-parse');
-import { Directory, serverPathToFile, clientPathToFile } from './ApiManagers/UploadManager';
+import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager';
import { red } from 'colors';
+const requestImageSize = require("../client/util/request-image-size");
export enum SizeSuffix {
Small = "_s",
@@ -27,6 +28,10 @@ export function InjectSize(filename: string, size: SizeSuffix) {
return filename.substring(0, filename.length - extension.length) + size + extension;
}
+function isLocal() {
+ return /Dash-Web[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/;
+}
+
export namespace DashUploadUtils {
export interface Size {
@@ -92,24 +97,18 @@ export namespace DashUploadUtils {
}
async function UploadPdf(absolutePath: string) {
- const dataBuffer = fs.readFileSync(absolutePath);
+ const dataBuffer = readFileSync(absolutePath);
const result: ParsedPDF = await parse(dataBuffer);
const parsedName = basename(absolutePath);
await new Promise<void>((resolve, reject) => {
const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
- const writeStream = fs.createWriteStream(serverPathToFile(Directory.text, textFilename));
+ const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
writeStream.write(result.text, error => error ? reject(error) : resolve());
});
return MoveParsedFile(absolutePath, Directory.pdfs);
}
- const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
- const sanitizeExtension = (source: string) => {
- let extension = path.extname(source);
- extension = extension.toLowerCase();
- extension = extension.split("?")[0];
- return extension;
- };
+ const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
/**
* Uploads an image specified by the @param source to Dash's /public/files/
@@ -134,12 +133,13 @@ export namespace DashUploadUtils {
};
export interface InspectionResults {
- isLocal: boolean;
- stream: any;
- normalizedUrl: string;
+ source: string;
+ requestable: string;
exifData: EnrichedExifData;
- contentSize?: number;
- contentType?: string;
+ contentSize: number;
+ contentType: string;
+ nativeWidth: number;
+ nativeHeight: number;
}
export interface EnrichedExifData {
@@ -152,6 +152,12 @@ export namespace DashUploadUtils {
return Promise.all(pending);
}
+ export interface RequestedImageSize {
+ width: number;
+ height: number;
+ type: string;
+ }
+
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
@@ -159,24 +165,28 @@ export namespace DashUploadUtils {
* @param source is the path or url to the image in question
*/
export const InspectImage = async (source: string): Promise<InspectionResults> => {
- const { isLocal, stream, normalized: normalizedUrl } = classify(source);
- const exifData = await parseExifData(source);
+ let resolvedUrl: string;
+ const matches = isLocal().exec(source);
+ if (matches === null) {
+ resolvedUrl = source;
+ } else {
+ resolvedUrl = `http://localhost:1050/${matches[1].split("\\").join("/")}`;
+ }
+ const exifData = await parseExifData(resolvedUrl);
const results = {
exifData,
- isLocal,
- stream,
- normalizedUrl
+ requestable: resolvedUrl
};
- // stop here if local, since request.head() can't handle local paths, only urls on the web
- if (isLocal) {
- return results;
- }
const { headers } = (await new Promise<any>((resolve, reject) => {
- request.head(source, (error, res) => error ? reject(error) : resolve(res));
- }));
+ request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res));
+ }).catch(error => console.error(error)));
+ const { width: nativeWidth, height: nativeHeight }: RequestedImageSize = await requestImageSize(resolvedUrl);
return {
+ source,
contentSize: parseInt(headers[size]),
contentType: headers[type],
+ nativeWidth,
+ nativeHeight,
...results
};
};
@@ -185,22 +195,20 @@ export namespace DashUploadUtils {
return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
const filename = basename(absolutePath);
const destinationPath = serverPathToFile(destination, filename);
- fs.rename(absolutePath, destinationPath, error => {
+ rename(absolutePath, destinationPath, error => {
resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
});
});
}
export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
- const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
- const resolved = filename || generate(prefix, normalizedUrl);
- const extension = format || sanitizeExtension(normalizedUrl || resolved);
+ const { requestable, source, ...remaining } = metadata;
+ const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
+ const resolved = filename || generate(prefix, extension);
const information: ImageUploadInformation = {
clientAccessPath: clientPathToFile(Directory.images, resolved),
serverAccessPaths: {},
- exifData,
- contentSize,
- contentType,
+ ...remaining
};
const { pngs, jpgs } = AcceptibleMedia;
return new Promise<ImageUploadInformation>(async (resolve, reject) => {
@@ -220,32 +228,22 @@ export namespace DashUploadUtils {
await new Promise<void>(resolve => {
const filename = InjectSize(resolved, suffix);
information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
- stream(normalizedUrl).pipe(resizer).pipe(fs.createWriteStream(serverPathToFile(Directory.images, filename)))
+ request(requestable).pipe(resizer).pipe(createWriteStream(serverPathToFile(Directory.images, filename)))
.on('close', resolve)
.on('error', reject);
});
}
- if (isLocal) {
- await new Promise<boolean>(resolve => {
- fs.unlink(normalizedUrl, error => resolve(error === null));
- });
+ if (isLocal().test(source)) {
+ unlinkSync(source);
}
resolve(information);
});
};
- const classify = (url: string) => {
- const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
- return {
- isLocal,
- stream: isLocal ? fs.createReadStream : request,
- normalized: isLocal ? path.normalize(url) : url
- };
- };
-
const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+ const image = await request.get(source, { encoding: null });
return new Promise<EnrichedExifData>(resolve => {
- new ExifImage(source, (error, data) => {
+ new ExifImage({ image }, (error, data) => {
let reason: Opt<string> = undefined;
if (error) {
reason = (error as any).code;
diff --git a/src/server/IDatabase.ts b/src/server/IDatabase.ts
new file mode 100644
index 000000000..6a63df485
--- /dev/null
+++ b/src/server/IDatabase.ts
@@ -0,0 +1,24 @@
+import * as mongodb from 'mongodb';
+import { Transferable } from './Message';
+
+export const DocumentsCollection = 'documents';
+export const NewDocumentsCollection = 'newDocuments';
+export interface IDatabase {
+ update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): Promise<void>;
+ updateMany(query: any, update: any, collectionName?: string): Promise<mongodb.WriteOpResult>;
+
+ replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): void;
+
+ delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+
+ deleteAll(collectionName?: string, persist?: boolean): Promise<any>;
+
+ insert(value: any, collectionName?: string): Promise<void>;
+
+ getDocument(id: string, fn: (result?: Transferable) => void, collectionName?: string): void;
+ getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName?: string): void;
+ visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName?: string): Promise<void>;
+
+ query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName?: string): Promise<mongodb.Cursor>;
+}
diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts
new file mode 100644
index 000000000..543f96e7f
--- /dev/null
+++ b/src/server/MemoryDatabase.ts
@@ -0,0 +1,100 @@
+import { IDatabase, DocumentsCollection, NewDocumentsCollection } from './IDatabase';
+import { Transferable } from './Message';
+import * as mongodb from 'mongodb';
+
+export class MemoryDatabase implements IDatabase {
+
+ private db: { [collectionName: string]: { [id: string]: any } } = {};
+
+ private getCollection(collectionName: string) {
+ const collection = this.db[collectionName];
+ if (collection) {
+ return collection;
+ } else {
+ return this.db[collectionName] = {};
+ }
+ }
+
+ public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> {
+ const collection = this.getCollection(collectionName);
+ const set = "$set";
+ if (set in value) {
+ let currentVal = collection[id] ?? (collection[id] = {});
+ const val = value[set];
+ for (const key in val) {
+ const keys = key.split(".");
+ for (let i = 0; i < keys.length - 1; i++) {
+ const k = keys[i];
+ if (typeof currentVal[k] === "object") {
+ currentVal = currentVal[k];
+ } else {
+ currentVal[k] = {};
+ currentVal = currentVal[k];
+ }
+ }
+ currentVal[keys[keys.length - 1]] = val[key];
+ }
+ } else {
+ collection[id] = value;
+ }
+ callback(null as any, {} as any);
+ return Promise.resolve(undefined);
+ }
+
+ public updateMany(query: any, update: any, collectionName = NewDocumentsCollection): Promise<mongodb.WriteOpResult> {
+ throw new Error("Can't updateMany a MemoryDatabase");
+ }
+
+ public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName = DocumentsCollection): void {
+ this.update(id, value, callback, upsert, collectionName);
+ }
+
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = DocumentsCollection): Promise<mongodb.DeleteWriteOpResultObject> {
+ const i = id.id ?? id;
+ delete this.getCollection(collectionName)[i];
+
+ return Promise.resolve({} as any);
+ }
+
+ public deleteAll(collectionName = DocumentsCollection, _persist = true): Promise<any> {
+ delete this.db[collectionName];
+ return Promise.resolve();
+ }
+
+ public insert(value: any, collectionName = DocumentsCollection): Promise<void> {
+ const id = value.id;
+ this.getCollection(collectionName)[id] = value;
+ return Promise.resolve();
+ }
+
+ public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = NewDocumentsCollection): void {
+ fn(this.getCollection(collectionName)[id]);
+ }
+ public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection): void {
+ fn(ids.map(id => this.getCollection(collectionName)[id]));
+ }
+
+ public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = NewDocumentsCollection): Promise<void> {
+ const visited = new Set<string>();
+ while (ids.length) {
+ const count = Math.min(ids.length, 1000);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
+ for (const doc of docs) {
+ const id = doc.id;
+ visited.add(id);
+ ids.push(...(await fn(doc)));
+ }
+ }
+ }
+
+ public query(): Promise<mongodb.Cursor> {
+ throw new Error("Can't query a MemoryDatabase");
+ }
+}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 621abfd1e..22d2fa8a8 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,4 +1,5 @@
import { Utils } from "../Utils";
+import { Image } from "canvas";
export class Message<T> {
private _name: string;
@@ -59,4 +60,5 @@ export namespace MessageStore {
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
+ export const AnalyzeInk = new Message<string>("Analyze Ink");
}
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 25259bd88..63e957cd1 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,6 +1,6 @@
import RouteSubscriber from "./RouteSubscriber";
import { DashUserModel } from "./authentication/models/user_model";
-import * as express from 'express';
+import { Request, Response, Express } from 'express';
import { cyan, red, green } from 'colors';
export enum Method {
@@ -9,12 +9,13 @@ export enum Method {
}
export interface CoreArguments {
- req: express.Request;
- res: express.Response;
+ req: Request;
+ res: Response;
isRelease: boolean;
}
-export type SecureHandler = (core: CoreArguments & { user: DashUserModel }) => any | Promise<any>;
+export type AuthorizedCore = CoreArguments & { user: DashUserModel };
+export type SecureHandler = (core: AuthorizedCore) => any | Promise<any>;
export type PublicHandler = (core: CoreArguments) => any | Promise<any>;
export type ErrorHandler = (core: CoreArguments & { error: any }) => any | Promise<any>;
@@ -34,7 +35,7 @@ enum RegistrationError {
}
export default class RouteManager {
- private server: express.Express;
+ private server: Express;
private _isRelease: boolean;
private failedRegistrations: { route: string, reason: RegistrationError }[] = [];
@@ -42,7 +43,7 @@ export default class RouteManager {
return this._isRelease;
}
- constructor(server: express.Express, isRelease: boolean) {
+ constructor(server: Express, isRelease: boolean) {
this.server = server;
this._isRelease = isRelease;
}
@@ -67,7 +68,7 @@ export default class RouteManager {
console.log('please remove all duplicate routes before continuing');
}
if (malformedCount) {
- console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$`);
+ console.log(`please ensure all routes adhere to ^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$`);
}
process.exit(1);
} else {
@@ -82,29 +83,34 @@ export default class RouteManager {
* @param initializer
*/
addSupervisedRoute = (initializer: RouteInitializer): void => {
- const { method, subscription, secureHandler: onValidation, publicHandler: onUnauthenticated, errorHandler: onError } = initializer;
+ const { method, subscription, secureHandler, publicHandler, errorHandler } = initializer;
+
const isRelease = this._isRelease;
- const supervised = async (req: express.Request, res: express.Response) => {
- const { user, originalUrl: target } = req;
+ const supervised = async (req: Request, res: Response) => {
+ let { user } = req;
+ const { originalUrl: target } = req;
+ if (process.env.DB === "MEM" && !user) {
+ user = { id: "guest", email: "", userDocumentId: "guestDocId" };
+ }
const core = { req, res, isRelease };
const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => {
try {
await toExecute(args);
} catch (e) {
console.log(red(target), user && ("email" in user) ? "<user logged out>" : undefined);
- if (onError) {
- onError({ ...core, error: e });
+ if (errorHandler) {
+ errorHandler({ ...core, error: e });
} else {
_error(res, `The server encountered an internal error when serving ${target}.`, e);
}
}
};
if (user) {
- await tryExecute(onValidation, { ...core, user });
+ await tryExecute(secureHandler, { ...core, user });
} else {
req.session!.target = target;
- if (onUnauthenticated) {
- await tryExecute(onUnauthenticated, core);
+ if (publicHandler) {
+ await tryExecute(publicHandler, core);
if (!res.headersSent) {
res.redirect("/login");
}
@@ -127,7 +133,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route
@@ -173,24 +179,24 @@ export const STATUS = {
PERMISSION_DENIED: 403
};
-export function _error(res: express.Response, message: string, error?: any) {
- console.error(message);
+export function _error(res: Response, message: string, error?: any) {
+ console.error(message, error);
res.statusMessage = message;
res.status(STATUS.EXECUTION_ERROR).send(error);
}
-export function _success(res: express.Response, body: any) {
+export function _success(res: Response, body: any) {
res.status(STATUS.OK).send(body);
}
-export function _invalid(res: express.Response, message: string) {
+export function _invalid(res: Response, message: string) {
res.statusMessage = message;
res.status(STATUS.BAD_REQUEST).send();
}
-export function _permission_denied(res: express.Response, message?: string) {
+export function _permission_denied(res: Response, message?: string) {
if (message) {
res.statusMessage = message;
}
- res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
+ res.status(STATUS.PERMISSION_DENIED).send("Permission Denied!");
}
diff --git a/src/server/Search.ts b/src/server/Search.ts
index 2b59c14b1..21064e520 100644
--- a/src/server/Search.ts
+++ b/src/server/Search.ts
@@ -1,4 +1,5 @@
import * as rp from 'request-promise';
+import { red } from 'colors';
const pathTo = (relative: string) => `http://localhost:8983/solr/dash/${relative}`;
@@ -43,7 +44,7 @@ export namespace Search {
export async function clear() {
try {
- return rp.post(pathTo("update"), {
+ await rp.post(pathTo("update"), {
body: {
delete: {
query: "*:*"
@@ -51,7 +52,10 @@ export namespace Search {
},
json: true
});
- } catch { }
+ } catch (e) {
+ console.log(red("Unable to clear search..."));
+ console.log(red(e.message));
+ }
}
export async function deleteDocuments(docs: string[]) {
diff --git a/src/server/Session/session.ts b/src/server/Session/session.ts
deleted file mode 100644
index 06a076ae4..000000000
--- a/src/server/Session/session.ts
+++ /dev/null
@@ -1,592 +0,0 @@
-import { red, cyan, green, yellow, magenta, blue, white } from "colors";
-import { on, fork, setupMaster, Worker, isMaster, isWorker } from "cluster";
-import { get } from "request-promise";
-import { Utils } from "../../Utils";
-import Repl, { ReplAction } from "../repl";
-import { readFileSync } from "fs";
-import { validate, ValidationError } from "jsonschema";
-import { configurationSchema } from "./session_config_schema";
-
-/**
- * This namespace relies on NodeJS's cluster module, which allows a parent (master) process to share
- * code with its children (workers). A simple `isMaster` flag indicates who is trying to access
- * the code, and thus determines the functionality that actually gets invoked (checked by the caller, not internally).
- *
- * Think of the master thread as a factory, and the workers as the helpers that actually run the server.
- *
- * So, when we run `npm start`, given the appropriate check, initializeMaster() is called in the parent process
- * This will spawn off its own child process (by default, mirrors the execution path of its parent),
- * in which initializeWorker() is invoked.
- */
-export namespace Session {
-
- export abstract class AppliedSessionAgent {
-
- // the following two methods allow the developer to create a custom
- // session and use the built in customization options for each thread
- protected abstract async launchMonitor(): Promise<Session.Monitor>;
- protected abstract async launchServerWorker(): Promise<Session.ServerWorker>;
-
- private launched = false;
-
- public killSession = (reason: string, graceful = true, errorCode = 0) => {
- const target = isMaster ? this.sessionMonitor : this.serverWorker;
- target.killSession(reason, graceful, errorCode);
- }
-
- private sessionMonitorRef: Session.Monitor | undefined;
- public get sessionMonitor(): Session.Monitor {
- if (!isMaster) {
- this.serverWorker.sendMonitorAction("kill", {
- graceful: false,
- reason: "Cannot access the session monitor directly from the server worker thread.",
- errorCode: 1
- });
- throw new Error();
- }
- return this.sessionMonitorRef!;
- }
-
- private serverWorkerRef: Session.ServerWorker | undefined;
- public get serverWorker(): Session.ServerWorker {
- if (isMaster) {
- throw new Error("Cannot access the server worker directly from the session monitor thread");
- }
- return this.serverWorkerRef!;
- }
-
- public async launch(): Promise<void> {
- if (!this.launched) {
- this.launched = true;
- if (isMaster) {
- this.sessionMonitorRef = await this.launchMonitor();
- } else {
- this.serverWorkerRef = await this.launchServerWorker();
- }
- } else {
- throw new Error("Cannot launch a session thread more than once per process.");
- }
- }
-
- }
-
- interface Configuration {
- showServerOutput: boolean;
- masterIdentifier: string;
- workerIdentifier: string;
- ports: { [description: string]: number };
- pollingRoute: string;
- pollingIntervalSeconds: number;
- pollingFailureTolerance: number;
- [key: string]: any;
- }
-
- const defaultConfiguration: Configuration = {
- showServerOutput: false,
- masterIdentifier: yellow("__monitor__:"),
- workerIdentifier: magenta("__server__:"),
- ports: { server: 3000 },
- pollingRoute: "/",
- pollingIntervalSeconds: 30,
- pollingFailureTolerance: 0
- };
-
- export type ExitHandler = (reason: Error | null) => void | Promise<void>;
-
- export namespace Monitor {
-
- export interface NotifierHooks {
- key?: (key: string) => (boolean | Promise<boolean>);
- crash?: (error: Error) => (boolean | Promise<boolean>);
- }
-
- export interface Action {
- message: string;
- args: any;
- }
-
- export type ServerMessageHandler = (action: Action) => void | Promise<void>;
-
- }
-
- /**
- * Validates and reads the configuration file, accordingly builds a child process factory
- * and spawns off an initial process that will respawn as predecessors die.
- */
- export class Monitor {
-
- private static count = 0;
- private exitHandlers: ExitHandler[] = [];
- private readonly notifiers: Monitor.NotifierHooks | undefined;
- private readonly configuration: Configuration;
- private onMessage: { [message: string]: Monitor.ServerMessageHandler[] | undefined } = {};
- private activeWorker: Worker | undefined;
- private key: string | undefined;
- private repl: Repl;
-
- public static Create(notifiers?: Monitor.NotifierHooks) {
- if (isWorker) {
- process.send?.({
- action: {
- message: "kill",
- args: {
- reason: "cannot create a monitor on the worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else if (++Monitor.count > 1) {
- console.error(red("cannot create more than one monitor."));
- process.exit(1);
- } else {
- return new Monitor(notifiers);
- }
- }
-
- /**
- * Kill this session and its active child
- * server process, either gracefully (may wait
- * indefinitely, but at least allows active networking
- * requests to complete) or immediately.
- */
- public killSession = async (reason: string, graceful = true, errorCode = 0) => {
- this.log(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`));
- this.log(`reason: ${(red(reason))}`);
- await this.executeExitHandlers(null);
- this.tryKillActiveWorker(graceful);
- process.exit(errorCode);
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Extend the default repl by adding in custom commands
- * that can invoke application logic external to this module
- */
- public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- this.repl.registerCommand(basename, argPatterns, action);
- }
-
- /**
- * Add a listener at this message. When the monitor process
- * receives a message, it will invoke all registered functions.
- */
- public addServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.push(handler);
- } else {
- this.onMessage[message] = [handler];
- }
- }
-
- /**
- * Unregister a given listener at this message.
- */
- public removeServerMessageListener = (message: string, handler: Monitor.ServerMessageHandler) => {
- const handlers = this.onMessage[message];
- if (handlers) {
- const index = handlers.indexOf(handler);
- if (index > -1) {
- handlers.splice(index, 1);
- }
- }
- }
-
- /**
- * Unregister all listeners at this message.
- */
- public clearServerMessageListeners = (message: string) => this.onMessage[message] = undefined;
-
- private constructor(notifiers?: Monitor.NotifierHooks) {
- this.notifiers = notifiers;
-
- console.log(this.timestamp(), cyan("initializing session..."));
-
- this.configuration = this.loadAndValidateConfiguration();
- this.initializeSessionKey();
- // determines whether or not we see the compilation / initialization / runtime output of each child server process
- setupMaster({ silent: !this.configuration.showServerOutput });
-
- // handle exceptions in the master thread - there shouldn't be many of these
- // the IPC (inter process communication) channel closed exception can't seem
- // to be caught in a try catch, and is inconsequential, so it is ignored
- process.on("uncaughtException", ({ message, stack }): void => {
- if (message !== "Channel closed") {
- this.log(red(message));
- if (stack) {
- this.log(`uncaught exception\n${red(stack)}`);
- }
- }
- });
-
- // a helpful cluster event called on the master thread each time a child process exits
- on("exit", ({ process: { pid } }, code, signal) => {
- const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`;
- this.log(cyan(prompt));
- // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one
- this.spawn();
- });
-
- this.repl = this.initializeRepl();
- this.spawn();
- }
-
-
- /**
- * Generates a blue UTC string associated with the time
- * of invocation.
- */
- private timestamp = () => blue(`[${new Date().toUTCString()}]`);
-
- /**
- * A formatted, identified and timestamped log in color
- */
- public log = (...optionalParams: any[]) => {
- console.log(this.timestamp(), this.configuration.masterIdentifier, ...optionalParams);
- }
-
- /**
- * If the caller has indicated an interest
- * in being notified of this feature, creates
- * a GUID for this session that can, for example,
- * be used as authentication for killing the server
- * (checked externally).
- */
- private initializeSessionKey = async (): Promise<void> => {
- if (this.notifiers?.key) {
- this.key = Utils.GenerateGuid();
- const success = await this.notifiers.key(this.key);
- const statement = success ? green("distributed session key to recipients") : red("distribution of session key failed");
- this.log(statement);
- }
- }
-
- /**
- * Builds the repl that allows the following commands to be typed into stdin of the master thread.
- */
- private initializeRepl = (): Repl => {
- const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.configuration.masterIdentifier}` });
- const boolean = /true|false/;
- const number = /\d+/;
- const letters = /[a-zA-Z]+/;
- repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
- repl.registerCommand("restart", [/clean|force/], args => this.tryKillActiveWorker(args[0] === "clean"));
- repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
- repl.registerCommand("set", [/polling/, number, boolean], args => {
- const newPollingIntervalSeconds = Math.floor(Number(args[2]));
- if (newPollingIntervalSeconds < 0) {
- this.log(red("the polling interval must be a non-negative integer"));
- } else {
- if (newPollingIntervalSeconds !== this.configuration.pollingIntervalSeconds) {
- this.configuration.pollingIntervalSeconds = newPollingIntervalSeconds;
- if (args[3] === "true") {
- this.activeWorker?.send({ newPollingIntervalSeconds });
- }
- }
- }
- });
- return repl;
- }
-
- /**
- * Reads in configuration .json file only once, in the master thread
- * and pass down any variables the pertinent to the child processes as environment variables.
- */
- private loadAndValidateConfiguration = (): Configuration => {
- try {
- console.log(this.timestamp(), cyan("validating configuration..."));
- const configuration: Configuration = JSON.parse(readFileSync('./session.config.json', 'utf8'));
- const options = {
- throwError: true,
- allowUnknownAttributes: false
- };
- // ensure all necessary and no excess information is specified by the configuration file
- validate(configuration, configurationSchema, options);
- let formatMaster = true;
- let formatWorker = true;
- Object.keys(defaultConfiguration).forEach(property => {
- if (!configuration[property]) {
- if (property === "masterIdentifier") {
- formatMaster = false;
- } else if (property === "workerIdentifier") {
- formatWorker = false;
- }
- configuration[property] = defaultConfiguration[property];
- }
- });
- if (formatMaster) {
- configuration.masterIdentifier = yellow(configuration.masterIdentifier + ":");
- }
- if (formatWorker) {
- configuration.workerIdentifier = magenta(configuration.workerIdentifier + ":");
- }
- return configuration;
- } catch (error) {
- if (error instanceof ValidationError) {
- console.log(red("\nSession configuration failed."));
- console.log("The given session.config.json configuration file is invalid.");
- console.log(`${error.instance}: ${error.stack}`);
- process.exit(0);
- } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
- console.log(cyan("Loading default session parameters..."));
- console.log("Consider including a session.config.json configuration file in your project root for customization.");
- return defaultConfiguration;
- } else {
- console.log(red("\nSession configuration failed."));
- console.log("The following unknown error occurred during configuration.");
- console.log(error.stack);
- process.exit(0);
- }
- }
- }
-
-
- private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Attempts to kill the active worker gracefully, unless otherwise specified.
- */
- private tryKillActiveWorker = (graceful = true): boolean => {
- if (!this.activeWorker?.isDead()) {
- if (graceful) {
- this.activeWorker?.send({ manualExit: true });
- } else {
- this.activeWorker?.process.kill();
- }
- return true;
- }
- return false;
- }
-
- /**
- * Allows the caller to set the port at which the target (be it the server,
- * the websocket, some other custom port) is listening. If an immediate restart
- * is specified, this monitor will kill the active child and re-launch the server
- * at the port. Otherwise, the updated port won't be used until / unless the child
- * dies on its own and triggers a restart.
- */
- private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => {
- if (value > 1023 && value < 65536) {
- this.configuration.ports[port] = value;
- if (immediateRestart) {
- this.tryKillActiveWorker();
- }
- } else {
- this.log(red(`${port} is an invalid port number`));
- }
- }
-
- /**
- * Kills the current active worker and proceeds to spawn a new worker,
- * feeding in configuration information as environment variables.
- */
- private spawn = (): void => {
- const {
- pollingRoute,
- pollingFailureTolerance,
- pollingIntervalSeconds,
- ports
- } = this.configuration;
- this.tryKillActiveWorker();
- this.activeWorker = fork({
- pollingRoute,
- pollingFailureTolerance,
- serverPort: ports.server,
- socketPort: ports.socket,
- pollingIntervalSeconds,
- session_key: this.key
- });
- this.log(cyan(`spawned new server worker with process id ${this.activeWorker.process.pid}`));
- // an IPC message handler that executes actions on the master thread when prompted by the active worker
- this.activeWorker.on("message", async ({ lifecycle, action }) => {
- if (action) {
- const { message, args } = action as Monitor.Action;
- console.log(this.timestamp(), `${this.configuration.workerIdentifier} action requested (${cyan(message)})`);
- switch (message) {
- case "kill":
- const { reason, graceful, errorCode } = args;
- this.killSession(reason, graceful, errorCode);
- break;
- case "notify_crash":
- if (this.notifiers?.crash) {
- const { error } = args;
- const success = await this.notifiers.crash(error);
- const statement = success ? green("distributed crash notification to recipients") : red("distribution of crash notification failed");
- this.log(statement);
- }
- break;
- case "set_port":
- const { port, value, immediateRestart } = args;
- this.setPort(port, value, immediateRestart);
- break;
- }
- const handlers = this.onMessage[message];
- if (handlers) {
- handlers.forEach(handler => handler({ message, args }));
- }
- } else if (lifecycle) {
- console.log(this.timestamp(), `${this.configuration.workerIdentifier} lifecycle phase (${lifecycle})`);
- }
- });
- }
-
- }
-
- /**
- * Effectively, each worker repairs the connection to the server by reintroducing a consistent state
- * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification
- * email if the server encounters an uncaught exception or if the server cannot be reached.
- */
- export class ServerWorker {
-
- private static count = 0;
- private shouldServerBeResponsive = false;
- private exitHandlers: ExitHandler[] = [];
- private pollingFailureCount = 0;
- private pollingIntervalSeconds: number;
- private pollingFailureTolerance: number;
- private pollTarget: string;
- private serverPort: number;
-
- public static Create(work: Function) {
- if (isMaster) {
- console.error(red("cannot create a worker on the monitor process."));
- process.exit(1);
- } else if (++ServerWorker.count > 1) {
- process.send?.({
- action: {
- message: "kill", args: {
- reason: "cannot create more than one worker on a given worker process.",
- graceful: false,
- errorCode: 1
- }
- }
- });
- process.exit(1);
- } else {
- return new ServerWorker(work);
- }
- }
-
- /**
- * Allows developers to invoke application specific logic
- * by hooking into the exiting of the server process.
- */
- public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
-
- /**
- * Kill the session monitor (parent process) from this
- * server worker (child process). This will also kill
- * this process (child process).
- */
- public killSession = (reason: string, graceful = true, errorCode = 0) => this.sendMonitorAction("kill", { reason, graceful, errorCode });
-
- /**
- * A convenience wrapper to tell the session monitor (parent process)
- * to carry out the action with the specified message and arguments.
- */
- public sendMonitorAction = (message: string, args?: any) => process.send!({ action: { message, args } });
-
- private constructor(work: Function) {
- this.lifecycleNotification(green(`initializing process... (${white(`${process.execPath} ${process.execArgv.join(" ")}`)})`));
-
- const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env;
- this.serverPort = Number(serverPort);
- this.pollingIntervalSeconds = Number(pollingIntervalSeconds);
- this.pollingFailureTolerance = Number(pollingFailureTolerance);
- this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`;
-
- this.configureProcess();
- work();
- this.pollServer();
- }
-
- /**
- * Set up message and uncaught exception handlers for this
- * server process.
- */
- private configureProcess = () => {
- // updates the local values of variables to the those sent from master
- process.on("message", async ({ newPollingIntervalSeconds, manualExit }) => {
- if (newPollingIntervalSeconds !== undefined) {
- this.pollingIntervalSeconds = newPollingIntervalSeconds;
- }
- if (manualExit !== undefined) {
- await this.executeExitHandlers(null);
- process.exit(0);
- }
- });
-
- // one reason to exit, as the process might be in an inconsistent state after such an exception
- process.on('uncaughtException', this.proactiveUnplannedExit);
- }
-
- /**
- * Execute the list of functions registered to be called
- * whenever the process exits.
- */
- private executeExitHandlers = async (reason: Error | null) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
-
- /**
- * Notify master thread (which will log update in the console) of initialization via IPC.
- */
- public lifecycleNotification = (event: string) => process.send?.({ lifecycle: event });
-
- /**
- * Called whenever the process has a reason to terminate, either through an uncaught exception
- * in the process (potentially inconsistent state) or the server cannot be reached.
- */
- private proactiveUnplannedExit = async (error: Error): Promise<void> => {
- this.shouldServerBeResponsive = false;
- // communicates via IPC to the master thread that it should dispatch a crash notification email
- this.sendMonitorAction("notify_crash", { error });
- await this.executeExitHandlers(error);
- // notify master thread (which will log update in the console) of crash event via IPC
- this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
- this.lifecycleNotification(red(error.message));
- process.exit(1);
- }
-
- /**
- * This monitors the health of the server by submitting a get request to whatever port / route specified
- * by the configuration every n seconds, where n is also given by the configuration.
- */
- private pollServer = async (): Promise<void> => {
- await new Promise<void>(resolve => {
- setTimeout(async () => {
- try {
- await get(this.pollTarget);
- if (!this.shouldServerBeResponsive) {
- // notify monitor thread that the server is up and running
- this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
- }
- this.shouldServerBeResponsive = true;
- resolve();
- } catch (error) {
- // if we expect the server to be unavailable, i.e. during compilation,
- // the listening variable is false, activeExit will return early and the child
- // process will continue
- if (this.shouldServerBeResponsive) {
- if (++this.pollingFailureCount > this.pollingFailureTolerance) {
- this.proactiveUnplannedExit(error);
- } else {
- this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`));
- }
- }
- }
- }, 1000 * this.pollingIntervalSeconds);
- });
- // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed
- this.pollServer();
- }
-
- }
-
-} \ No newline at end of file
diff --git a/src/server/Session/session_config_schema.ts b/src/server/Session/session_config_schema.ts
deleted file mode 100644
index 5a85a45e3..000000000
--- a/src/server/Session/session_config_schema.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Schema } from "jsonschema";
-
-export const configurationSchema: Schema = {
- id: "/configuration",
- type: "object",
- properties: {
- ports: {
- type: "object",
- properties: {
- server: { type: "number", minimum: 1024, maximum: 65535 },
- socket: { type: "number", minimum: 1024, maximum: 65535 }
- },
- required: ["server"],
- additionalProperties: true
- },
- pollingRoute: {
- type: "string",
- pattern: /\/[a-zA-Z]*/g
- },
- masterIdentifier: {
- type: "string",
- minLength: 1
- },
- workerIdentifier: {
- type: "string",
- minLength: 1
- },
- showServerOutput: { type: "boolean" },
- pollingIntervalSeconds: {
- type: "number",
- minimum: 1,
- maximum: 86400
- },
- pollingFailureTolerance: {
- type: "number",
- minimum: 0,
- }
- }
-}; \ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index 578147d60..f485e1dcd 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -10,6 +10,16 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
import { logPort } from "../ActionUtilities";
import { timeMap } from "../ApiManagers/UserManager";
import { green } from "colors";
+import { Image } from "canvas";
+import { write, createWriteStream } from "fs";
+import { serverPathToFile, Directory } from "../ApiManagers/UploadManager";
+const tesseract = require("node-tesseract-ocr");
+const config = {
+ lang: "eng",
+ oem: 1,
+ psm: 8
+};
+const imageDataUri = require('image-data-uri');
export namespace WebSocket {
@@ -51,6 +61,7 @@ export namespace WebSocket {
Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
+ Utils.AddServerHandlerCallback(socket, MessageStore.AnalyzeInk, RecognizeImage);
Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
@@ -68,6 +79,17 @@ export namespace WebSocket {
logPort("websocket", socketPort);
}
+ async function RecognizeImage([query, callback]: [string, (result: any) => any]) {
+ const path = serverPathToFile(Directory.images, "handwriting.jpg");
+ imageDataUri.outputFile(query, path).then((savedName: string) => {
+ console.log("saved " + savedName);
+ const remadePath = path.split("\\").join("\\\\");
+ tesseract.recognize(remadePath, config)
+ .then(callback)
+ .catch(console.log);
+ });
+ }
+
function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
const { ProjectCredentials } = GoogleCredentialsLoader;
switch (query.type) {
@@ -83,16 +105,25 @@ export namespace WebSocket {
export async function deleteFields() {
await Database.Instance.deleteAll();
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
await Database.Instance.deleteAll('newDocuments');
}
+ // export async function deleteUserDocuments() {
+ // await Database.Instance.deleteAll();
+ // await Database.Instance.deleteAll('newDocuments');
+ // }
+
export async function deleteAll() {
await Database.Instance.deleteAll();
await Database.Instance.deleteAll('newDocuments');
await Database.Instance.deleteAll('sessions');
await Database.Instance.deleteAll('users');
- await Search.clear();
+ if (process.env.DISABLE_SEARCH !== "true") {
+ await Search.clear();
+ }
}
function barReceived(socket: SocketIO.Socket, userEmail: string) {
@@ -115,8 +146,6 @@ export namespace WebSocket {
socket.broadcast.emit(MessageStore.SetField.Message, newValue));
if (newValue.type === Types.Text) {
Search.updateDocument({ id: newValue.id, data: (newValue as any).data });
- console.log("set field");
- console.log("checking in");
}
}
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 8b8b1e029..b0ea2f9ad 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -14,6 +14,7 @@ import { Utils } from "../../../Utils";
import { nullAudio } from "../../../new_fields/URLField";
import { DragManager } from "../../../client/util/DragManager";
import { InkingControl } from "../../../client/views/InkingControl";
+import { CollectionViewType } from "../../../client/views/collections/CollectionView";
export class CurrentUserUtils {
private static curr_id: string;
@@ -33,29 +34,29 @@ export class CurrentUserUtils {
// a default set of note types .. not being used yet...
static setupNoteTypes(doc: Doc) {
return [
- Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplateDoc: true }),
- Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplateDoc: true }),
- Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }),
- Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }),
- Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplateDoc: true })
+ Docs.Create.TextDocument("", { title: "Note", backgroundColor: "yellow", isTemplateDoc: true }),
+ Docs.Create.TextDocument("", { title: "Idea", backgroundColor: "pink", isTemplateDoc: true }),
+ Docs.Create.TextDocument("", { title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }),
+ Docs.Create.TextDocument("", { title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }),
+ Docs.Create.TextDocument("", { title: "Todo", backgroundColor: "orange", isTemplateDoc: true })
];
}
// setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
static setupCreatorButtons(doc: Doc, buttons?: string[]) {
const notes = CurrentUserUtils.setupNoteTypes(doc);
- doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
+ doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 });
doc.activePen = doc;
const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
- { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
- { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { width: 250, height: 250, title: "container" })' },
+ { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" })' },
+ { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
{ title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
- { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
- { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
- { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { width: 200, title: "ready to record audio" })` },
- { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
- { title: "presentation", icon: "tv", ignoreClick: true, drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
- { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+ { title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
+ { title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
+ { title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
+ { title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
+ { title: "presentation", icon: "tv", ignoreClick: true, drag: `Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { _width: 200, _height: 500, _viewType: ${CollectionViewType.Stacking}, title: "a presentation trail" })` },
+ { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
{ title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
{ title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
@@ -65,7 +66,7 @@ export class CurrentUserUtils {
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
];
return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -103,7 +104,7 @@ export class CurrentUserUtils {
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
];
return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -111,13 +112,17 @@ export class CurrentUserUtils {
}
static setupThumbButtons(doc: Doc) {
- const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+ const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
{ title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
];
return docProtoData.map(data => Docs.Create.FontIconDocument({
- nativeWidth: 10, nativeHeight: 10, width: 10, height: 10, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, _dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
+ clipboard: data.clipboard,
onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -126,16 +131,18 @@ export class CurrentUserUtils {
static setupThumbDoc(userDoc: Doc) {
if (!userDoc.thumbDoc) {
- return Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
- width: 100, height: 50, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5, isExpanded: true
+ const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
+ _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, isExpanded: true, backgroundColor: "white"
});
+ thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", isExpanded: true, flexDirection: "column" });
+ userDoc.thumbDoc = thumbDoc;
}
- return userDoc.thumbDoc;
+ return Cast(userDoc.thumbDoc, Doc);
}
static setupMobileDoc(userDoc: Doc) {
return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
- columnWidth: 100, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons", autoHeight: true, yMargin: 5
+ columnWidth: 100, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5
});
}
@@ -143,18 +150,19 @@ export class CurrentUserUtils {
static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
// setup a masonry view of all he creators
const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
- width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons",
- dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 5
+ _width: 500, _autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons",
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _yMargin: 5
});
// setup a color picker
const color = Docs.Create.ColorDocument({
- title: "color picker", width: 400, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+ title: "color picker", _width: 300, _dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
});
return Docs.Create.ButtonDocument({
- width: 35, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
+ _width: 35, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
+ letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
- width: 500, height: 800, lockedPosition: true, chromeStatus: "disabled", title: "tools stack"
+ _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack"
}),
onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
});
@@ -164,22 +172,23 @@ export class CurrentUserUtils {
static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
// setup workspaces library item
doc.workspaces = Docs.Create.TreeDocument([], {
- title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
});
doc.documents = Docs.Create.TreeDocument([], {
- title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
});
// setup Recently Closed library item
doc.recentlyClosed = Docs.Create.TreeDocument([], {
- title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
});
return Docs.Create.ButtonDocument({
- width: 50, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Library", fontSize: 10,
+ _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Library", fontSize: 10,
+ letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
- title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
+ title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, _dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
}),
targetContainer: sidebarContainer,
onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
@@ -189,7 +198,8 @@ export class CurrentUserUtils {
// setup the Search button which will display the search panel.
static setupSearchPanel(sidebarContainer: Doc) {
return Docs.Create.ButtonDocument({
- width: 50, height: 35, backgroundColor: "#222222", color: "lightgrey", title: "Search", fontSize: 10,
+ _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Search", fontSize: 10,
+ letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.QueryDocument({
title: "search stack", ignoreClick: true
}),
@@ -202,7 +212,7 @@ export class CurrentUserUtils {
// setup the list of sidebar mode buttons which determine what is displayed in the sidebar
static setupSidebarButtons(doc: Doc) {
doc.sidebarContainer = new Doc();
- (doc.sidebarContainer as Doc).chromeStatus = "disabled";
+ (doc.sidebarContainer as Doc)._chromeStatus = "disabled";
(doc.sidebarContainer as Doc).onClick = ScriptField.MakeScript("freezeSidebar()");
doc.ToolsBtn = this.setupToolsPanel(doc.sidebarContainer as Doc, doc);
@@ -211,20 +221,21 @@ export class CurrentUserUtils {
// Finally, setup the list of buttons to display in the sidebar
doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.ToolsBtn as Doc], {
- width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
- backgroundColor: "lightgrey", chromeStatus: "disabled", title: "library stack"
+ _width: 500, _height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
+ backgroundColor: "rgb(100, 100, 100)", _chromeStatus: "disabled", title: "library stack",
+ _yMargin: 10,
});
}
/// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
static setupExpandingButtons(doc: Doc) {
doc.undoBtn = Docs.Create.FontIconDocument(
- { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
+ { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
doc.redoBtn = Docs.Create.FontIconDocument(
- { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
+ { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
- title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0",
+ title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0",
backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
});
@@ -233,13 +244,13 @@ export class CurrentUserUtils {
// sets up the default set of documents to be shown in the Overlay layer
static setupOverlays(doc: Doc) {
doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" });
- doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" });
+ doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, _width: 500, _height: 370, title: "Link Follower" });
Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc);
}
// the initial presentation Doc to use
static setupDefaultPresentation(doc: Doc) {
- doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", boxShadow: "0 0" });
+ doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, boxShadow: "0 0" });
}
static setupMobileUploads(doc: Doc) {
@@ -371,4 +382,4 @@ export class CurrentUserUtils {
};
return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined);
}
-} \ No newline at end of file
+}
diff --git a/src/server/database.ts b/src/server/database.ts
index 6e0771c11..83ce865c6 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -5,6 +5,8 @@ import { Utils, emptyFunction } from '../Utils';
import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
+import { IDatabase } from './IDatabase';
+import { MemoryDatabase } from './MemoryDatabase';
import * as mongoose from 'mongoose';
export namespace Database {
@@ -44,7 +46,7 @@ export namespace Database {
}
}
- class Database {
+ class Database implements IDatabase {
public static DocumentsCollection = 'documents';
private MongoClient = mongodb.MongoClient;
private currentWrites: { [id: string]: Promise<void> } = {};
@@ -215,7 +217,7 @@ export namespace Database {
if (!fetchIds.length) {
continue;
}
- const docs = await new Promise<{ [key: string]: any }[]>(res => Instance.getDocuments(fetchIds, res, "newDocuments"));
+ const docs = await new Promise<{ [key: string]: any }[]>(res => this.getDocuments(fetchIds, res, collectionName));
for (const doc of docs) {
const id = doc.id;
visited.add(id);
@@ -262,7 +264,16 @@ export namespace Database {
}
}
- export const Instance = new Database();
+ function getDatabase() {
+ switch (process.env.DB) {
+ case "MEM":
+ return new MemoryDatabase();
+ default:
+ return new Database();
+ }
+ }
+
+ export const Instance: IDatabase = getDatabase();
export namespace Auxiliary {
@@ -331,4 +342,4 @@ export namespace Database {
}
-} \ No newline at end of file
+}
diff --git a/src/server/index.ts b/src/server/index.ts
index 6b3dfd614..313a2f0e2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5,11 +5,11 @@ import * as path from 'path';
import { Database } from './database';
import { DashUploadUtils } from './DashUploadUtils';
import RouteSubscriber from './RouteSubscriber';
-import initializeServer from './server_initialization';
+import initializeServer from './server_Initialization';
import RouteManager, { Method, _success, _permission_denied, _error, _invalid, PublicHandler } from './RouteManager';
import * as qs from 'query-string';
import UtilManager from './ApiManagers/UtilManager';
-import { SearchManager, SolrManager } from './ApiManagers/SearchManager';
+import { SearchManager } from './ApiManagers/SearchManager';
import UserManager from './ApiManagers/UserManager';
import { WebSocket } from './Websocket/Websocket';
import DownloadManager from './ApiManagers/DownloadManager';
@@ -17,16 +17,17 @@ import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
import DeleteManager from "./ApiManagers/DeleteManager";
import PDFManager from "./ApiManagers/PDFManager";
import UploadManager from "./ApiManagers/UploadManager";
-import { log_execution, Email } from "./ActionUtilities";
+import { log_execution } from "./ActionUtilities";
import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
import { Logger } from "./ProcessFactory";
-import { yellow, red } from "colors";
-import { Session } from "./Session/session";
-import { DashSessionAgent } from "./DashSession";
+import { yellow } from "colors";
+import { DashSessionAgent } from "./DashSession/DashSessionAgent";
+import SessionManager from "./ApiManagers/SessionManager";
+import { AppliedSessionAgent } from "resilient-server-session";
export const onWindows = process.platform === "win32";
-export let sessionAgent: Session.AppliedSessionAgent;
+export let sessionAgent: AppliedSessionAgent;
export const publicDirectory = path.resolve(__dirname, "public");
export const filesDirectory = path.resolve(publicDirectory, "files");
@@ -40,11 +41,13 @@ async function preliminaryFunctions() {
await GoogleCredentialsLoader.loadCredentials();
GoogleApiServerUtils.processProjectCredentials();
await DashUploadUtils.buildFileDirectories();
- await log_execution({
- startMessage: "attempting to initialize mongodb connection",
- endMessage: "connection outcome determined",
- action: Database.tryInitializeConnection
- });
+ if (process.env.DB !== "MEM") {
+ await log_execution({
+ startMessage: "attempting to initialize mongodb connection",
+ endMessage: "connection outcome determined",
+ action: Database.tryInitializeConnection
+ });
+ }
}
/**
@@ -58,6 +61,7 @@ async function preliminaryFunctions() {
*/
function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) {
const managers = [
+ new SessionManager(),
new UserManager(),
new UploadManager(),
new DownloadManager(),
@@ -88,21 +92,6 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
secureHandler: ({ res }) => res.send(true)
});
- addSupervisedRoute({
- method: Method.GET,
- subscription: new RouteSubscriber("kill").add("key"),
- secureHandler: ({ req, res }) => {
- if (req.params.key === process.env.session_key) {
- res.send("<img src='https://media.giphy.com/media/NGIfqtcS81qi4/giphy.gif' style='width:100%;height:100%;'/>");
- setTimeout(() => {
- sessionAgent.killSession("an authorized user has manually ended the server session via the /kill route", false);
- }, 5000);
- } else {
- res.redirect("/home");
- }
- }
- });
-
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -155,4 +144,4 @@ if (process.env.RELEASE) {
(sessionAgent = new DashSessionAgent()).launch();
} else {
launchServer();
-} \ No newline at end of file
+}
diff --git a/src/server/repl.ts b/src/server/repl.ts
deleted file mode 100644
index c4526528e..000000000
--- a/src/server/repl.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { createInterface, Interface } from "readline";
-import { red, green, white } from "colors";
-
-export interface Configuration {
- identifier: () => string | string;
- onInvalid?: (command: string, validCommand: boolean) => string | string;
- onValid?: (success?: string) => string | string;
- isCaseSensitive?: boolean;
-}
-
-export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
-export interface Registration {
- argPatterns: RegExp[];
- action: ReplAction;
-}
-
-export default class Repl {
- private identifier: () => string | string;
- private onInvalid: ((command: string, validCommand: boolean) => string) | string;
- private onValid: ((success: string) => string) | string;
- private isCaseSensitive: boolean;
- private commandMap = new Map<string, Registration[]>();
- public interface: Interface;
- private busy = false;
- private keys: string | undefined;
-
- constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
- this.identifier = prompt;
- this.onInvalid = onInvalid || this.usage;
- this.onValid = onValid || this.success;
- this.isCaseSensitive = isCaseSensitive ?? true;
- this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
- }
-
- private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
-
- private usage = (command: string, validCommand: boolean) => {
- if (validCommand) {
- const formatted = white(command);
- const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n'));
- return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
- } else {
- const resolved = this.keys;
- if (resolved) {
- return resolved;
- }
- const members: string[] = [];
- const keys = this.commandMap.keys();
- let next: IteratorResult<string>;
- while (!(next = keys.next()).done) {
- members.push(next.value);
- }
- return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
- }
- }
-
- private success = (command: string) => `${this.resolvedIdentifier()} completed execution of ${white(command)}`;
-
- public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
- const existing = this.commandMap.get(basename);
- const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
- const registration = { argPatterns: converted, action };
- if (existing) {
- existing.push(registration);
- } else {
- this.commandMap.set(basename, [registration]);
- }
- }
-
- private invalid = (command: string, validCommand: boolean) => {
- console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand)));
- this.busy = false;
- }
-
- private valid = (command: string) => {
- console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
- this.busy = false;
- }
-
- private considerInput = async (line: string) => {
- if (this.busy) {
- console.log(red("Busy"));
- return;
- }
- this.busy = true;
- line = line.trim();
- if (this.isCaseSensitive) {
- line = line.toLowerCase();
- }
- const [command, ...args] = line.split(/\s+/g);
- if (!command) {
- return this.invalid(command, false);
- }
- const registered = this.commandMap.get(command);
- if (registered) {
- const { length } = args;
- const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
- for (const { argPatterns, action } of candidates) {
- const parsed: string[] = [];
- let matched = true;
- if (length) {
- for (let i = 0; i < length; i++) {
- let matches: RegExpExecArray | null;
- if ((matches = argPatterns[i].exec(args[i])) === null) {
- matched = false;
- break;
- }
- parsed.push(matches[0]);
- }
- }
- if (!length || matched) {
- await action(parsed);
- this.valid(`${command} ${parsed.join(" ")}`);
- return;
- }
- }
- this.invalid(command, true);
- } else {
- this.invalid(command, false);
- }
- }
-
-} \ No newline at end of file
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index cbe070293..9f67c1dda 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -86,7 +86,7 @@ function buildWithMiddleware(server: express.Express) {
resave: true,
cookie: { maxAge: week },
saveUninitialized: true,
- store: new MongoStore({ url: Database.url })
+ store: process.env.DB === "MEM" ? new session.MemoryStore() : new MongoStore({ url: Database.url })
}),
flash(),
expressFlash(),
@@ -152,4 +152,4 @@ function registerCorsProxy(server: express.Express) {
});
}).pipe(res);
});
-} \ No newline at end of file
+}
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index cc68e8a4d..281bb3217 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -2,8 +2,6 @@
declare module 'googlephotos';
declare module 'react-image-lightbox-with-rotate';
-declare module 'kill-port';
-declare module 'ipc-event-emitter';
declare module 'cors';
declare module '@react-pdf/renderer' {