aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts30
-rw-r--r--src/client/documents/Documents.ts84
-rw-r--r--src/client/util/DocumentManager.ts2
-rw-r--r--src/client/util/DragManager.ts168
-rw-r--r--src/client/util/DropConverter.ts4
-rw-r--r--src/client/util/RichTextSchema.tsx1263
-rw-r--r--src/client/util/SelectionManager.ts5
-rw-r--r--src/client/views/ContextMenu.scss1
-rw-r--r--src/client/views/DocComponent.tsx11
-rw-r--r--src/client/views/DocumentButtonBar.tsx2
-rw-r--r--src/client/views/DocumentDecorations.scss42
-rw-r--r--src/client/views/DocumentDecorations.tsx66
-rw-r--r--src/client/views/GestureOverlay.tsx2
-rw-r--r--src/client/views/InkingControl.tsx2
-rw-r--r--src/client/views/MainView.scss6
-rw-r--r--src/client/views/MainView.tsx24
-rw-r--r--src/client/views/MetadataEntryMenu.scss9
-rw-r--r--src/client/views/ScriptBox.tsx4
-rw-r--r--src/client/views/SearchDocBox.tsx10
-rw-r--r--src/client/views/animationtimeline/Keyframe.scss105
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx560
-rw-r--r--src/client/views/animationtimeline/Timeline.scss322
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx622
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.scss94
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx77
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.scss107
-rw-r--r--src/client/views/animationtimeline/TimelineOverview.tsx181
-rw-r--r--src/client/views/animationtimeline/Track.scss15
-rw-r--r--src/client/views/animationtimeline/Track.tsx380
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx36
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx6
-rw-r--r--src/client/views/collections/CollectionMapView.tsx8
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx137
-rw-r--r--src/client/views/collections/CollectionPileView.tsx9
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx4
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx14
-rw-r--r--src/client/views/collections/CollectionStackingView.scss7
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx54
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx10
-rw-r--r--src/client/views/collections/CollectionSubView.tsx90
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx4
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx67
-rw-r--r--src/client/views/collections/CollectionView.tsx44
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss11
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx95
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx6
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx21
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx22
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx4
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx39
-rw-r--r--src/client/views/nodes/AudioBox.tsx14
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx30
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx31
-rw-r--r--src/client/views/nodes/DocumentBox.scss3
-rw-r--r--src/client/views/nodes/DocumentBox.tsx151
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx52
-rw-r--r--src/client/views/nodes/DocumentView.scss1
-rw-r--r--src/client/views/nodes/DocumentView.tsx94
-rw-r--r--src/client/views/nodes/FieldView.tsx8
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx14
-rw-r--r--src/client/views/nodes/PDFBox.scss1
-rw-r--r--src/client/views/nodes/PresBox.scss29
-rw-r--r--src/client/views/nodes/PresBox.tsx223
-rw-r--r--src/client/views/nodes/QueryBox.tsx3
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx6
-rw-r--r--src/client/views/nodes/VideoBox.tsx6
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx95
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx269
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss36
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx207
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx162
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss (renamed from src/client/views/nodes/FormattedTextBox.scss)2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx (renamed from src/client/views/nodes/FormattedTextBox.tsx)180
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss (renamed from src/client/views/nodes/FormattedTextBoxComment.scss)0
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx (renamed from src/client/views/nodes/FormattedTextBoxComment.tsx)30
-rw-r--r--src/client/views/nodes/formattedText/ImageResizeView.tsx138
-rw-r--r--src/client/views/nodes/formattedText/ParagraphNodeSpec.ts (renamed from src/client/util/ParagraphNodeSpec.ts)6
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts (renamed from src/client/util/ProsemirrorExampleTransfer.ts)12
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.scss (renamed from src/client/util/RichTextMenu.scss)45
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx (renamed from src/client/util/RichTextMenu.tsx)22
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts (renamed from src/client/util/RichTextRules.ts)24
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx537
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx81
-rw-r--r--src/client/views/nodes/formattedText/TooltipTextMenu.scss (renamed from src/client/util/TooltipTextMenu.scss)0
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts296
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts264
-rw-r--r--src/client/views/nodes/formattedText/prosemirrorPatches.js (renamed from src/client/util/prosemirrorPatches.js)0
-rw-r--r--src/client/views/nodes/formattedText/schema_rts.ts26
-rw-r--r--src/client/views/pdf/PDFViewer.scss4
-rw-r--r--src/client/views/presentationview/PresElementBox.scss43
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx134
-rw-r--r--src/client/views/search/SearchItem.tsx12
-rw-r--r--src/mobile/MobileInterface.tsx2
-rw-r--r--src/new_fields/Doc.ts35
-rw-r--r--src/new_fields/RichTextUtils.ts12
-rw-r--r--src/new_fields/Types.ts4
-rw-r--r--src/new_fields/documentSchemas.ts98
-rw-r--r--src/scraping/buxton/final/BuxtonImporter.ts232
-rw-r--r--src/server/DashUploadUtils.ts7
-rw-r--r--src/server/Websocket/Websocket.ts7
-rw-r--r--src/server/authentication/models/current_user_utils.ts116
107 files changed, 6329 insertions, 2382 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index ad12c68a1..23b59ac9d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -4,7 +4,7 @@ import { Socket, Room } from 'socket.io';
import { Message } from './server/Message';
export namespace Utils {
- export const DRAG_THRESHOLD = 4;
+ export let DRAG_THRESHOLD = 4;
export function GenerateGuid(): string {
return v4();
@@ -512,7 +512,7 @@ export function setupMoveUpEvents(
(target as any)._downY = (target as any)._lastY = e.clientY;
const _moveEvent = (e: PointerEvent): void => {
- if (Math.abs(e.clientX - (target as any)._downX) > 4 || Math.abs(e.clientY - (target as any)._downY) > 4) {
+ if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
if (moveEvent(e, [(target as any)._downX, (target as any)._downY],
[e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) {
document.removeEventListener("pointermove", _moveEvent);
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 8c0149a89..ff471853a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -10,7 +10,7 @@ import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/Share
import { Utils } from "../../../Utils";
import { Docs, DocumentOptions } from "../../documents/Documents";
import { Networking } from "../../Network";
-import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox";
import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
import Photos = require('googlephotos');
@@ -76,7 +76,6 @@ export namespace GooglePhotos {
}
export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
- await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const { collection, title, descriptionKey, tag } = options;
const dataDocument = Doc.GetProto(collection);
const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
@@ -157,24 +156,20 @@ export namespace GooglePhotos {
images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE));
const values = Object.values(ContentCategories);
for (const value of values) {
- if (value !== ContentCategories.NONE) {
- const results = await ContentSearch({ included: [value] });
- if (results.mediaItems) {
- const ids = results.mediaItems.map(item => item.id);
- for (const id of ids) {
- const image = await Cast(idMapping[id], Doc);
- if (image) {
- const key = image[Id];
- const tags = tagMapping.get(key)!;
- if (!tags.includes(value)) {
- tagMapping.set(key, tags + delimiter + value);
- }
- }
- }
+ if (value === ContentCategories.NONE) {
+ continue;
+ }
+ for (const id of (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id)) {
+ const image = await Cast(idMapping[id], Doc);
+ if (!image) {
+ continue;
}
+ const key = image[Id];
+ const tags = tagMapping.get(key);
+ !tags?.includes(value) && tagMapping.set(key, tags + delimiter + value);
}
}
- images && images.forEach(image => {
+ images?.forEach(image => {
const concatenated = tagMapping.get(image[Id])!;
const tags = concatenated.split(delimiter);
if (tags.length > 1) {
@@ -184,7 +179,6 @@ export namespace GooglePhotos {
image.googlePhotosTags = ContentCategories.NONE;
}
});
-
};
interface DateRange {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c64916897..45687f597 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,7 @@
import { CollectionView } from "../views/collections/CollectionView";
import { CollectionViewType } from "../views/collections/CollectionView";
import { AudioBox } from "../views/nodes/AudioBox";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
import { ImageBox } from "../views/nodes/ImageBox";
import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { PDFBox } from "../views/nodes/PDFBox";
@@ -75,12 +75,15 @@ export interface DocumentOptions {
_itemIndex?: number; // which item index the carousel viewer is showing
_showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
_singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document)
+ "_carousel-caption-xMargin"?: number;
+ "_carousel-caption-yMargin"?: number;
x?: number;
y?: number;
z?: number;
author?: string;
dropAction?: dropActionType;
childDropAction?: dropActionType;
+ targetDropAction?: dropActionType;
layoutKey?: string;
type?: string;
title?: string;
@@ -90,12 +93,16 @@ export interface DocumentOptions {
scale?: number;
isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
forceActive?: boolean;
- layout?: string | Doc;
+ layout?: string | Doc; // default layout string for a document
+ childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox or Buxton layout in tree view)
+ childLayoutString?: string; // template string for collection to use to render its children
hideFilterView?: boolean; // whether to hide the filter popout on collections
hideHeadings?: boolean; // whether stacking view column headings should be hidden
isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: boolean;
+ targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc)
templates?: List<string>;
+ hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images)
backgroundColor?: string | ScriptField; // background color for data doc
_backgroundColor?: string | ScriptField; // background color for each template layout doc ( overrides backgroundColor )
color?: string; // foreground color data doc
@@ -117,7 +124,8 @@ export interface DocumentOptions {
displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
borderRounding?: string;
boxShadow?: string;
- dontRegisterChildren?: boolean;
+ dontRegisterChildViews?: boolean;
+ "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form
"onClick-rawScript"?: string; // onClick script in raw text form
"onCheckedClick-rawScript"?: string; // onChecked script in raw text form
"onCheckedClick-params"?: List<string>; // parameter list for onChecked treeview functions
@@ -131,7 +139,9 @@ export interface DocumentOptions {
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;
+ onDoubleClick?: ScriptField;
onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
+ onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
@@ -377,6 +387,15 @@ export namespace Docs {
*/
export namespace Create {
+ /**
+ * Synchronously returns a collection into which
+ * the device documents will be put. This is initially empty,
+ * but gets populated by updates from the web socket. When everything is over,
+ * this function cleans up after itself.
+ * s
+ * Look at Websocket.ts for the server-side counterpart to this
+ * function.
+ */
export function Buxton() {
let responded = false;
const loading = new Doc;
@@ -389,9 +408,13 @@ export namespace Docs {
});
const parentProto = Doc.GetProto(parent);
const { _socket } = DocServer;
+
+ // just in case, clean up
_socket.off(MessageStore.BuxtonDocumentResult.Message);
_socket.off(MessageStore.BuxtonImportComplete.Message);
- Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, errors }) => {
+
+ // this is where the client handles the receipt of a new valid parsed document
+ Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, invalid: errors }) => {
if (!responded) {
responded = true;
parentProto.data = new List<Doc>();
@@ -406,10 +429,10 @@ export namespace Docs {
_nativeWidth: nativeWidth,
_nativeHeight: nativeHeight
}));
- const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true });
- const deviceProto = Doc.GetProto(doc);
- deviceProto.hero = new ImageField(constructed[0].url);
- Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: deviceProto } });
+ // the main document we create
+ const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true, hero: new ImageField(constructed[0].url) });
+ // add the parsed attributes to this main document
+ Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
Doc.AddDocToList(parentProto, "data", doc);
} else if (errors) {
console.log(errors);
@@ -417,14 +440,17 @@ export namespace Docs {
alert("A Buxton document import was completely empty (??)");
}
});
+
+ // when the import is complete, we stop listening for these creation
+ // and termination events and alert the user
Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => {
_socket.off(MessageStore.BuxtonDocumentResult.Message);
_socket.off(MessageStore.BuxtonImportComplete.Message);
alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`);
});
const startTime = Date.now();
- Utils.Emit(_socket, MessageStore.BeginBuxtonImport, "");
- return parent;
+ Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); // signal the server to start importing
+ return parent; // synchronously return the collection, to be populateds
}
Scripting.addGlobal(Buxton);
@@ -468,7 +494,7 @@ export namespace Docs {
const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey);
const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
- viewDoc.type !== DocumentType.LINK && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "audio timeline"));
+ viewDoc.type !== DocumentType.LINK && DocUtils.MakeLinkToActiveAudio(viewDoc);
return Doc.assign(viewDoc, delegateProps, true);
}
@@ -541,8 +567,23 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options);
}
- export function TextDocument(text: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.RTF), text, options, undefined, "text");
+ export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = "text") {
+ const rtf = {
+ doc: {
+ type: "doc", content: [{
+ type: "paragraph",
+ content: [{
+ type: "text",
+ text
+ }]
+ }]
+ },
+ selection: { type: "text", anchor: 1, head: 1 },
+ storedMarks: []
+ };
+
+ const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined;
+ return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
@@ -553,9 +594,7 @@ export namespace Docs {
linkDocProto.anchor1_timecode = source.doc.currentTimecode || source.doc.displayTimecode;
linkDocProto.anchor2_timecode = target.doc.currentTimecode || target.doc.displayTimecode;
- if (linkDocProto.layout_key1 === undefined) {
- Cast(linkDocProto.proto, Doc, null).layout_key1 = LinkAnchorBox.LayoutString("anchor1");
- Cast(linkDocProto.proto, Doc, null).layout_key2 = LinkAnchorBox.LayoutString("anchor2");
+ if (linkDocProto.linkBoxExcludedKeys === undefined) {
Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]);
Cast(linkDocProto.proto, Doc, null).layoutKey = undefined;
}
@@ -607,7 +646,7 @@ export namespace Docs {
}
export function DocumentDocument(document?: Doc, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.DOCHOLDER), document, { title: document ? document.title + "" : "container", ...options });
+ return InstanceFromProto(Prototypes.get(DocumentType.DOCHOLDER), document, { title: document ? document.title + "" : "container", targetDropAction: "move", ...options });
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
@@ -708,6 +747,10 @@ export namespace Docs {
};
return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id);
}
+
+ export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
+ return InstanceFromProto(proto, undefined, options);
+ }
}
export namespace Get {
@@ -963,6 +1006,11 @@ export namespace DocUtils {
});
}
+ export let ActiveRecordings: Doc[] = [];
+
+ export function MakeLinkToActiveAudio(doc: Doc) {
+ DocUtils.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: doc }, { doc: d }, "audio link", "audio timeline"));
+ }
export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
@@ -1014,3 +1062,5 @@ export namespace DocUtils {
}
Scripting.addGlobal("Docs", Docs);
+Scripting.addGlobal(function makeDelegate(proto: any) { const d = Docs.Create.DelegateDocument(proto, { title: "child of " + proto.title }); return d; });
+
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4683e77a8..1ba6f0248 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -117,7 +117,7 @@ export class DocumentManager {
pairs.push(...linksList.reduce((pairs, link) => {
const linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) {
+ if (dv.props.Document.type !== DocumentType.LINK || dv.props.LayoutTemplateString !== docView1.props.LayoutTemplateString) {
pairs.push({ a: dv, b: docView1, l: link });
}
});
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 35694a6bd..d91eb60ef 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,22 +1,15 @@
-import { Doc, Field, DocListCast } from "../../new_fields/Doc";
-import { Cast, ScriptCast, StrCast } from "../../new_fields/Types";
-import { emptyFunction } from "../../Utils";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { DocumentManager } from "./DocumentManager";
-import { LinkManager } from "./LinkManager";
-import { SelectionManager } from "./SelectionManager";
-import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { Docs, DocUtils } from "../documents/Documents";
-import { ScriptField } from "../../new_fields/ScriptField";
+import { action, observable, runInAction } from "mobx";
+import { DateField } from "../../new_fields/DateField";
+import { Doc, Field, Opt } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { PrefetchProxy } from "../../new_fields/Proxy";
import { listSpec } from "../../new_fields/Schema";
-import { Scripting } from "./Scripting";
-import { convertDropDataToButtons } from "./DropConverter";
-import { AudioBox } from "../views/nodes/AudioBox";
-import { DateField } from "../../new_fields/DateField";
-import { DocumentView } from "../views/nodes/DocumentView";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { Cast, NumCast, ScriptCast, StrCast } from "../../new_fields/Types";
+import { emptyFunction } from "../../Utils";
+import { Docs, DocUtils } from "../documents/Documents";
+import * as globalCssVariables from "../views/globalCssVariables.scss";
import { UndoManager } from "./UndoManager";
export type dropActionType = "alias" | "copy" | "move" | undefined; // undefined = move
@@ -53,10 +46,10 @@ export function SetupDrag(
const onItemDown = async (e: React.PointerEvent) => {
if (e.button === 0) {
e.stopPropagation();
- if (e.shiftKey && CollectionDockingView.Instance) {
+ if (e.shiftKey) {
e.persist();
const dragDoc = await docFunc();
- dragDoc && CollectionDockingView.Instance.StartOtherDrag({
+ dragDoc && DragManager.Vals.Instance.StartWindowDrag?.({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
@@ -73,6 +66,19 @@ export function SetupDrag(
export namespace DragManager {
let dragDiv: HTMLDivElement;
+ export class Vals {
+ static Instance: Vals = new Vals();
+ @observable public IsDragging: boolean = false;
+ @observable public horizSnapLines: number[] = [];
+ @observable public vertSnapLines: number[] = [];
+ public StartWindowDrag: Opt<((e: any, dragDocs: Doc[]) => void)> = undefined;
+ public SetIsDragging(dragging: boolean) { runInAction(() => this.IsDragging = dragging); }
+ public GetIsDragging() { return this.IsDragging; }
+ public clearSnapLines() {
+ this.vertSnapLines.length = 0;
+ this.horizSnapLines.length = 0;
+ }
+ }
export function Root() {
const root = document.getElementById("root");
@@ -180,7 +186,8 @@ export namespace DragManager {
export function MakeDropTarget(
element: HTMLElement,
dropFunc: (e: Event, de: DropEvent) => void,
- doc?: Doc
+ doc?: Doc,
+ preDropFunc?: (e: Event, de: DropEvent) => void,
): DragDropDisposer {
if ("canDrop" in element.dataset) {
throw new Error(
@@ -194,6 +201,7 @@ export namespace DragManager {
if (de.complete.docDragData && doc?.targetDropAction) {
de.complete.docDragData.dropAction = StrCast(doc.targetDropAction) as dropActionType;
}
+ preDropFunc?.(e, de);
};
element.addEventListener("dashOnDrop", handler);
doc && element.addEventListener("dashPreDrop", preDropHandler);
@@ -208,7 +216,7 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
const addAudioTag = (dropDoc: any) => {
dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
- dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline"));
+ dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(dropDoc);
return dropDoc;
};
const batch = UndoManager.StartBatch("dragging");
@@ -239,40 +247,6 @@ export namespace DragManager {
StartDrag(eles, new DragManager.DocumentDragData([]), downX, downY, options, finishDrag);
}
- // drag links and drop link targets (aliasing them if needed)
- export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
- const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
-
- if (draggedDocs.length) {
- const moddrag: Doc[] = [];
- for (const draggedDoc of draggedDocs) {
- const doc = await Cast(draggedDoc.annotationOn, Doc);
- if (doc) moddrag.push(doc);
- }
-
- const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
- dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => {
- docView.props.removeDocument?.(doc);
- addDocument(doc);
- return true;
- };
- const containingView = docView.props.ContainingCollectionView;
- const finishDrag = (e: DragCompleteEvent) =>
- e.docDragData && (e.docDragData.droppedDocuments =
- dragData.draggedDocuments.reduce((droppedDocs, d) => {
- const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView);
- if (dvs.length) {
- dvs.forEach(dv => droppedDocs.push(dv.props.Document));
- } else {
- droppedDocs.push(Doc.MakeAlias(d));
- }
- return droppedDocs;
- }, [] as Doc[]));
-
- StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag);
- }
- }
-
// drag&drop the pdf annotation anchor which will create a text note on drop via a dropCompleted() DragOption
export function StartPdfAnnoDrag(eles: HTMLElement[], dragData: PdfAnnoDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag(eles, dragData, downX, downY, options);
@@ -292,7 +266,32 @@ export namespace DragManager {
StartDrag([ele], {}, downX, downY);
}
- function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
+ export function SetSnapLines(horizLines: number[], vertLines: number[]) {
+ DragManager.Vals.Instance.horizSnapLines.push(...horizLines);
+ DragManager.Vals.Instance.vertSnapLines.push(...vertLines);
+ }
+
+ export function snapDrag(e: PointerEvent, xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number) {
+ const snapThreshold = NumCast(Doc.UserDoc()["constants-snapThreshold"], 10);
+ const snapVal = (pts: number[], drag: number, snapLines: number[]) => {
+ if (snapLines.length) {
+ const offs = [pts[0], (pts[0] - pts[1]) / 2, -pts[1]]; // offsets from drag pt
+ const rangePts = [drag - offs[0], drag - offs[1], drag - offs[2]]; // left, mid, right or top, mid, bottom pts to try to snap to snaplines
+ const closestPts = rangePts.map(pt => snapLines.reduce((nearest, curr) => Math.abs(nearest - pt) > Math.abs(curr - pt) ? curr : nearest));
+ const closestDists = rangePts.map((pt, i) => Math.abs(pt - closestPts[i]));
+ const minIndex = closestDists[0] < closestDists[1] && closestDists[0] < closestDists[2] ? 0 : closestDists[1] < closestDists[2] ? 1 : 2;
+ return closestDists[minIndex] < snapThreshold ? closestPts[minIndex] + offs[minIndex] : drag;
+ }
+ return drag;
+ };
+
+ return {
+ thisX: snapVal([xFromLeft, xFromRight], e.pageX, DragManager.Vals.Instance.vertSnapLines),
+ thisY: snapVal([yFromTop, yFromBottom], e.pageY, DragManager.Vals.Instance.horizSnapLines)
+ };
+ }
+ export let docsBeingDragged: Doc[] = [];
+ export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
eles = eles.filter(e => e);
if (!dragDiv) {
dragDiv = document.createElement("div");
@@ -300,19 +299,29 @@ export namespace DragManager {
dragDiv.style.pointerEvents = "none";
DragManager.Root().appendChild(dragDiv);
}
- SelectionManager.SetIsDragging(true);
+ DragManager.Vals.Instance.SetIsDragging(true);
const scaleXs: number[] = [];
const scaleYs: number[] = [];
const xs: number[] = [];
const ys: number[] = [];
- const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : [];
+ docsBeingDragged = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof PdfAnnoDragData ? [dragData.dragDocument] : [];
+ const elesCont = {
+ left: Number.MAX_SAFE_INTEGER,
+ top: Number.MAX_SAFE_INTEGER,
+ right: Number.MIN_SAFE_INTEGER,
+ bottom: Number.MIN_SAFE_INTEGER
+ };
const dragElements = eles.map(ele => {
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : ele.cloneNode(true) as HTMLElement;
const rect = ele.getBoundingClientRect();
const scaleX = rect.width / ele.offsetWidth,
scaleY = rect.height / ele.offsetHeight;
+ elesCont.left = Math.min(rect.left, elesCont.left);
+ elesCont.top = Math.min(rect.top, elesCont.top);
+ elesCont.right = Math.max(rect.right, elesCont.right);
+ elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
xs.push(rect.left);
ys.push(rect.top);
scaleXs.push(scaleX);
@@ -332,7 +341,7 @@ export namespace DragManager {
dragElement.style.width = `${rect.width / scaleX}px`;
dragElement.style.height = `${rect.height / scaleY}px`;
- if (docs.length) {
+ if (docsBeingDragged.length) {
const pdfBox = dragElement.getElementsByTagName("canvas");
const pdfBoxSrc = ele.getElementsByTagName("canvas");
Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
@@ -362,31 +371,38 @@ export namespace DragManager {
let lastX = downX;
let lastY = downY;
+ const xFromLeft = downX - elesCont.left;
+ const yFromTop = downY - elesCont.top;
+ const xFromRight = elesCont.right - downX;
+ const yFromBottom = elesCont.bottom - downY;
let alias = "alias";
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
}
- if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
+ if (e.shiftKey && dragData.droppedDocuments.length === 1) {
!dragData.dropAction && (dragData.dropAction = alias);
if (dragData.dropAction === "move") {
dragData.removeDocument?.(dragData.draggedDocuments[0]);
}
AbortDrag();
finishDrag?.(new DragCompleteEvent(true, dragData));
- CollectionDockingView.Instance.StartOtherDrag({
+ DragManager.Vals.Instance.StartWindowDrag?.({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
}, dragData.droppedDocuments);
}
+
+ const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom);
+
alias = "move";
- const moveX = e.pageX - lastX;
- const moveY = e.pageY - lastY;
- lastX = e.pageX;
- lastY = e.pageY;
+ const moveX = thisX - lastX;
+ const moveY = thisY - lastY;
+ lastX = thisX;
+ lastY = thisY;
dragElements.map((dragElement, i) => (dragElement.style.transform =
`translate(${(xs[i] += moveX) + (options?.offsetX || 0)}px, ${(ys[i] += moveY) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`)
);
@@ -396,21 +412,22 @@ export namespace DragManager {
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
eles.map(ele => ele.parentElement && ele.parentElement?.className === dragData.dragDivName ? (ele.parentElement.hidden = false) : (ele.hidden = false));
};
- const endDrag = () => {
+ const endDrag = action(() => {
document.removeEventListener("pointermove", moveHandler, true);
document.removeEventListener("pointerup", upHandler);
- };
+ DragManager.Vals.Instance.clearSnapLines();
+ });
AbortDrag = () => {
hideDragShowOriginalElements();
- SelectionManager.SetIsDragging(false);
+ DragManager.Vals.Instance.SetIsDragging(false);
options?.dragComplete?.(new DragCompleteEvent(true, dragData));
endDrag();
};
const upHandler = (e: PointerEvent) => {
hideDragShowOriginalElements();
- dispatchDrag(eles, e, dragData, options, finishDrag);
- SelectionManager.SetIsDragging(false);
+ dispatchDrag(eles, e, dragData, xFromLeft, yFromTop, xFromRight, yFromBottom, options, finishDrag);
+ DragManager.Vals.Instance.SetIsDragging(false);
endDrag();
options?.dragComplete?.(new DragCompleteEvent(false, dragData));
};
@@ -418,7 +435,8 @@ export namespace DragManager {
document.addEventListener("pointerup", upHandler);
}
- function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) {
+ function dispatchDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any },
+ xFromLeft: number, yFromTop: number, xFromRight: number, yFromBottom: number, options?: DragOptions, finishDrag?: (e: DragCompleteEvent) => void) {
const removed = dragData.dontHideOnDrop ? [] : dragEles.map(dragEle => {
const ret = { ele: dragEle, w: dragEle.style.width, h: dragEle.style.height, o: dragEle.style.overflow };
dragEle.style.width = "0";
@@ -432,14 +450,15 @@ export namespace DragManager {
r.ele.style.height = r.h;
r.ele.style.overflow = r.o;
});
+ const { thisX, thisY } = snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom);
if (target) {
const complete = new DragCompleteEvent(false, dragData);
target.dispatchEvent(
new CustomEvent<DropEvent>("dashPreDrop", {
bubbles: true,
detail: {
- x: e.x,
- y: e.y,
+ x: thisX,
+ y: thisY,
complete: complete,
shiftKey: e.shiftKey,
altKey: e.altKey,
@@ -453,8 +472,8 @@ export namespace DragManager {
new CustomEvent<DropEvent>("dashOnDrop", {
bubbles: true,
detail: {
- x: e.x,
- y: e.y,
+ x: thisX,
+ y: thisY,
complete: complete,
shiftKey: e.shiftKey,
altKey: e.altKey,
@@ -466,4 +485,3 @@ export namespace DragManager {
}
}
}
-Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); });
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 60a6bbb3c..d6db882b8 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -7,6 +7,7 @@ import { Docs } from "../documents/Documents";
import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
import { RichTextField } from "../../new_fields/RichTextField";
import { ImageField } from "../../new_fields/URLField";
+import { Scripting } from "./Scripting";
//
// converts 'doc' into a template that can be used to render other documents.
@@ -68,10 +69,11 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
});
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
- dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
} else if (doc.isButtonBar) {
dbox.ignoreClick = true;
}
data.droppedDocuments[i] = dbox;
});
}
+Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); }); \ No newline at end of file
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
deleted file mode 100644
index d5dd05aa4..000000000
--- a/src/client/util/RichTextSchema.tsx
+++ /dev/null
@@ -1,1263 +0,0 @@
-import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { baseKeymap, toggleMark } from "prosemirror-commands";
-import { redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
-import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
-import { StepMap } from "prosemirror-transform";
-import { EditorView } from "prosemirror-view";
-import * as ReactDOM from 'react-dom';
-import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../new_fields/Doc";
-import { Id } from "../../new_fields/FieldSymbols";
-import { List } from "../../new_fields/List";
-import { ObjectField } from "../../new_fields/ObjectField";
-import { listSpec } from "../../new_fields/Schema";
-import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { ComputedField } from "../../new_fields/ScriptField";
-import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../Utils";
-import { DocServer } from "../DocServer";
-import { Docs } from "../documents/Documents";
-import { CollectionViewType } from "../views/collections/CollectionView";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { DocumentManager } from "./DocumentManager";
-import ParagraphNodeSpec from "./ParagraphNodeSpec";
-import { Transform } from "./Transform";
-import React = require("react");
-
-const
- blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0],
- hrDOM: DOMOutputSpecArray = ["hr"],
- preDOM: DOMOutputSpecArray = ["pre", ["code", 0]],
- brDOM: DOMOutputSpecArray = ["br"],
- ulDOM: DOMOutputSpecArray = ["ul", 0];
-
-// :: Object
-// [Specs](#model.NodeSpec) for the nodes defined in this schema.
-export const nodes: { [index: string]: NodeSpec } = {
- // :: NodeSpec The top level document node.
- doc: {
- content: "block+"
- },
-
- footnote: {
- group: "inline",
- content: "inline*",
- inline: true,
- attrs: {
- visibility: { default: false }
- },
- // This makes the view treat the node as a leaf, even though it
- // technically has content
- atom: true,
- toDOM: () => ["footnote", 0],
- parseDOM: [{ tag: "footnote" }]
- },
-
- paragraph: ParagraphNodeSpec,
-
- // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
- blockquote: {
- content: "block+",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "blockquote" }],
- toDOM() { return blockquoteDOM; }
- },
-
- // :: NodeSpec A horizontal rule (`<hr>`).
- horizontal_rule: {
- group: "block",
- parseDOM: [{ tag: "hr" }],
- toDOM() { return hrDOM; }
- },
-
- // :: NodeSpec A heading textblock, with a `level` attribute that
- // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
- // `<h6>` elements.
- heading: {
- attrs: { level: { default: 1 } },
- content: "inline*",
- group: "block",
- defining: true,
- parseDOM: [{ tag: "h1", attrs: { level: 1 } },
- { tag: "h2", attrs: { level: 2 } },
- { tag: "h3", attrs: { level: 3 } },
- { tag: "h4", attrs: { level: 4 } },
- { tag: "h5", attrs: { level: 5 } },
- { tag: "h6", attrs: { level: 6 } }],
- toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
- },
-
- // :: NodeSpec A code listing. Disallows marks or non-text inline
- // nodes by default. Represented as a `<pre>` element with a
- // `<code>` element inside of it.
- code_block: {
- content: "text*",
- marks: "",
- group: "block",
- code: true,
- defining: true,
- parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
- toDOM() { return preDOM; }
- },
-
- // :: NodeSpec The text node.
- text: {
- group: "inline"
- },
-
- dashComment: {
- attrs: {
- docid: { default: "" },
- },
- inline: true,
- group: "inline",
- toDOM(node) {
- const attrs = { style: `width: 40px` };
- return ["span", { ...node.attrs, ...attrs }, "←"];
- },
- },
-
- summary: {
- inline: true,
- attrs: {
- visibility: { default: false },
- text: { default: undefined },
- textslice: { default: undefined },
- },
- group: "inline",
- toDOM(node) {
- const attrs = { style: `width: 40px` };
- return ["span", { ...node.attrs, ...attrs }];
- },
- },
-
- // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
- // `alt`, and `href` attributes. The latter two default to the empty
- // string.
- image: {
- inline: true,
- attrs: {
- src: {},
- agnostic: { default: null },
- width: { default: 100 },
- alt: { default: null },
- title: { default: null },
- float: { default: "left" },
- location: { default: "onRight" },
- docid: { default: "" }
- },
- group: "inline",
- draggable: true,
- parseDOM: [{
- tag: "img[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt"),
- width: Math.min(100, Number(dom.getAttribute("width"))),
- };
- }
- }],
- // TODO if we don't define toDom, dragging the image crashes. Why?
- toDOM(node) {
- const attrs = { style: `width: ${node.attrs.width}` };
- return ["img", { ...node.attrs, ...attrs }];
- }
- },
-
- dashDoc: {
- inline: true,
- attrs: {
- width: { default: 200 },
- height: { default: 100 },
- title: { default: null },
- 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,
- toDOM(node) {
- const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
- return ["div", { ...node.attrs, ...attrs }];
- }
- },
-
- video: {
- inline: true,
- attrs: {
- src: {},
- width: { default: "100px" },
- alt: { default: null },
- title: { default: null }
- },
- group: "inline",
- draggable: true,
- parseDOM: [{
- tag: "video[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt"),
- width: Math.min(100, Number(dom.getAttribute("width"))),
- };
- }
- }],
- toDOM(node) {
- const attrs = { style: `width: ${node.attrs.width}` };
- return ["video", { ...node.attrs, ...attrs }];
- }
- },
-
- // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
- hard_break: {
- inline: true,
- group: "inline",
- selectable: false,
- parseDOM: [{ tag: "br" }],
- toDOM() { return brDOM; }
- },
-
- ordered_list: {
- ...orderedList,
- content: 'list_item+',
- group: 'block',
- attrs: {
- bulletStyle: { default: 0 },
- mapStyle: { default: "decimal" },
- setFontSize: { default: undefined },
- setFontFamily: { default: "inherit" },
- setFontColor: { default: "inherit" },
- inheritedFontSize: { default: undefined },
- visibility: { default: true },
- indent: { default: undefined }
- },
- toDOM(node: Node<any>) {
- if (node.attrs.mapStyle === "bullet") return ['ul', 0];
- const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
- const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
- const ffam = node.attrs.setFontFamily;
- const color = node.attrs.setFontColor;
- return node.attrs.visibility ?
- ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
- }
- },
-
- bullet_list: {
- ...bulletList,
- content: 'list_item+',
- group: 'block',
- // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
- toDOM(node: Node<any>) {
- return ['ul', 0];
- }
- },
-
- list_item: {
- attrs: {
- bulletStyle: { default: 0 },
- mapStyle: { default: "decimal" },
- visibility: { default: true }
- },
- ...listItem,
- content: 'paragraph block*',
- toDOM(node: any) {
- const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
- return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
- //return ["li", { class: `${map}` }, 0];
- }
- },
-};
-
-const emDOM: DOMOutputSpecArray = ["em", 0];
-const strongDOM: DOMOutputSpecArray = ["strong", 0];
-const codeDOM: DOMOutputSpecArray = ["code", 0];
-
-// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
-export const marks: { [index: string]: MarkSpec } = {
- // :: MarkSpec A link. Has `href` and `title` attributes. `title`
- // defaults to the empty string. Rendered and parsed as an `<a>`
- // element.
- link: {
- attrs: {
- href: {},
- targetId: { default: "" },
- linkId: { default: "" },
- showPreview: { default: true },
- location: { default: null },
- title: { default: null },
- docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
- },
- inclusive: false,
- parseDOM: [{
- tag: "a[href]", getAttrs(dom: any) {
- return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") };
- }
- }],
- toDOM(node: any) {
- return node.attrs.docref && node.attrs.title ?
- ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
- ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
- }
- },
-
-
- // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
- pFontColor: {
- attrs: {
- color: { default: "#000" }
- },
- inclusive: true,
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- return { color: dom.getAttribute("color") };
- }
- }],
- toDOM(node: any) {
- return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0];
- }
- },
-
- marker: {
- attrs: {
- highlight: { default: "transparent" }
- },
- inclusive: true,
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- return { highlight: dom.getAttribute("backgroundColor") };
- }
- }],
- toDOM(node: any) {
- return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
- }
- },
-
- // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
- // Has parse rules that also match `<i>` and `font-style: italic`.
- em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
- toDOM() { return emDOM; }
- },
-
- // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
- // also match `<b>` and `font-weight: bold`.
- strong: {
- parseDOM: [{ tag: "strong" },
- { tag: "b" },
- { style: "font-weight" }],
- toDOM() { return strongDOM; }
- },
-
- strikethrough: {
- parseDOM: [
- { tag: 'strike' },
- { style: 'text-decoration=line-through' },
- { style: 'text-decoration-line=line-through' }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration-line:line-through'
- }]
- },
-
- subscript: {
- excludes: 'superscript',
- parseDOM: [
- { tag: 'sub' },
- { style: 'vertical-align=sub' }
- ],
- toDOM: () => ['sub']
- },
-
- superscript: {
- excludes: 'subscript',
- parseDOM: [
- { tag: 'sup' },
- { style: 'vertical-align=super' }
- ],
- toDOM: () => ['sup']
- },
-
- mbulletType: {
- attrs: {
- bulletType: { default: "decimal" }
- },
- toDOM(node: any) {
- return ['span', {
- style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
- }];
- }
- },
-
- metadata: {
- toDOM() {
- return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
- }
- },
- metadataKey: {
- toDOM() {
- return ['span', { style: 'font-style:italic; ' }];
- }
- },
- metadataVal: {
- toDOM() {
- return ['span'];
- }
- },
-
- summarizeInclusive: {
- parseDOM: [
- {
- tag: "span",
- getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
- const style = getComputedStyle(p);
- if (style.textDecoration === "underline") return null;
- if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
- p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
- return null;
- }
- }
- return false;
- }
- },
- ],
- inclusive: true,
- toDOM() {
- return ['span', {
- style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
- }];
- }
- },
-
- summarize: {
- inclusive: false,
- parseDOM: [
- {
- tag: "span",
- getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
- const style = getComputedStyle(p);
- if (style.textDecoration === "underline") return null;
- if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
- p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
- return null;
- }
- }
- return false;
- }
- },
- ],
- toDOM() {
- return ['span', {
- style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
- }];
- }
- },
-
- underline: {
- parseDOM: [
- {
- tag: "span",
- getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
- const style = getComputedStyle(p);
- if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
- return null;
- }
- }
- return false;
- }
- }
- // { style: "text-decoration=underline" }
- ],
- toDOM: () => ['span', {
- style: 'text-decoration:underline;text-decoration-style:line'
- }]
- },
-
- search_highlight: {
- attrs: {
- selected: { default: false }
- },
- parseDOM: [{ style: 'background: yellow' }],
- toDOM(node: any) {
- return ['span', {
- style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
- }];
- }
- },
-
- // the id of the user who entered the text
- user_mark: {
- attrs: {
- userid: { default: "" },
- modified: { default: "when?" }, // 1 second intervals since 1970
- },
- group: "inline",
- toDOM(node: any) {
- const uid = node.attrs.userid.replace(".", "").replace("@", "");
- const min = Math.round(node.attrs.modified / 12);
- const hr = Math.round(min / 60);
- const day = Math.round(hr / 60 / 24);
- const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " UM-remote" : "";
- return ['span', { class: "UM-" + uid + remote + " UM-min-" + min + " UM-hr-" + hr + " UM-day-" + day }, 0];
- }
- },
- // the id of the user who entered the text
- user_tag: {
- attrs: {
- userid: { default: "" },
- modified: { default: "when?" }, // 1 second intervals since 1970
- tag: { default: "" }
- },
- group: "inline",
- inclusive: false,
- toDOM(node: any) {
- const uid = node.attrs.userid.replace(".", "").replace("@", "");
- return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
- }
- },
-
-
- // :: MarkSpec Code font mark. Represented as a `<code>` element.
- code: {
- parseDOM: [{ tag: "code" }],
- toDOM() { return codeDOM; }
- },
-
- /* FONTS */
- pFontFamily: {
- attrs: {
- family: { default: "Crimson Text" },
- },
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- const cstyle = getComputedStyle(dom);
- if (cstyle.font) {
- if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
- if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
- if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
- if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
- if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
- if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
- }
- }
- }],
- toDOM: (node) => ['span', {
- style: `font-family: "${node.attrs.family}";`
- }]
- },
-
- /** FONT SIZES */
- pFontSize: {
- attrs: {
- fontSize: { default: 10 }
- },
- parseDOM: [{ style: 'font-size: 10px;' }],
- toDOM: (node) => ['span', {
- style: `font-size: ${node.attrs.fontSize}px;`
- }]
- },
-};
-
-export class ImageResizeView {
- _handle: HTMLElement;
- _img: HTMLElement;
- _outer: HTMLElement;
- constructor(node: any, view: any, getPos: any, addDocTab: any) {
- this._handle = document.createElement("span");
- this._img = document.createElement("img");
- this._outer = document.createElement("span");
- this._outer.style.position = "relative";
- this._outer.style.width = node.attrs.width;
- this._outer.style.height = node.attrs.height;
- this._outer.style.display = "inline-block";
- this._outer.style.overflow = "hidden";
- (this._outer.style as any).float = node.attrs.float;
-
- this._img.setAttribute("src", node.attrs.src);
- this._img.style.width = "100%";
- this._handle.style.position = "absolute";
- this._handle.style.width = "20px";
- this._handle.style.height = "20px";
- this._handle.style.backgroundColor = "blue";
- this._handle.style.borderRadius = "15px";
- this._handle.style.display = "none";
- this._handle.style.bottom = "-10px";
- this._handle.style.right = "-10px";
- const self = this;
- this._img.onclick = function (e: any) {
- e.stopPropagation();
- e.preventDefault();
- if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
- view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
- }
- };
- this._img.onpointerdown = function (e: any) {
- if (e.ctrlKey) {
- e.preventDefault();
- e.stopPropagation();
- DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
- (linkDoc instanceof Doc) &&
- DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
- document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false));
- }
- };
- this._handle.onpointerdown = function (e: any) {
- e.preventDefault();
- e.stopPropagation();
- const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
- const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
- const startX = e.pageX;
- const startWidth = parseFloat(node.attrs.width);
- const onpointermove = (e: any) => {
- const currentX = e.pageX;
- const diffInPx = currentX - startX;
- self._outer.style.width = `${startWidth + diffInPx}`;
- self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
- };
-
- const onpointerup = () => {
- document.removeEventListener("pointermove", onpointermove);
- document.removeEventListener("pointerup", onpointerup);
- const pos = view.state.selection.from;
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
- view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
- };
-
- document.addEventListener("pointermove", onpointermove);
- document.addEventListener("pointerup", onpointerup);
- };
-
- this._outer.appendChild(this._img);
- this._outer.appendChild(this._handle);
- (this as any).dom = this._outer;
- }
-
- selectNode() {
- this._img.classList.add("ProseMirror-selectednode");
-
- this._handle.style.display = "";
- }
-
- deselectNode() {
- this._img.classList.remove("ProseMirror-selectednode");
-
- this._handle.style.display = "none";
- }
-}
-
-
-export class DashDocCommentView {
- _collapsed: HTMLElement;
- _view: any;
- constructor(node: any, view: any, getPos: any) {
- this._collapsed = document.createElement("span");
- this._collapsed.className = "formattedTextBox-inlineComment";
- this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
- this._view = view;
- const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
- for (let i = getPos() + 1; i < view.state.doc.content.size; i++) {
- const m = view.state.doc.nodeAt(i);
- if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) {
- return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
- }
- }
- const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" });
- view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc));
- setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0);
- return undefined;
- };
- this._collapsed.onpointerdown = (e: any) => {
- e.stopPropagation();
- };
- this._collapsed.onpointerup = (e: any) => {
- const target = targetNode();
- 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
- 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) { }
- }, 0);
- }
- e.stopPropagation();
- };
- this._collapsed.onpointerenter = (e: any) => {
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
- e.preventDefault();
- e.stopPropagation();
- };
- this._collapsed.onpointerleave = (e: any) => {
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
- e.preventDefault();
- e.stopPropagation();
- };
- (this as any).dom = this._collapsed;
- }
- selectNode() { }
-}
-
-export class DashDocView {
- _dashSpan: HTMLDivElement;
- _outer: HTMLElement;
- _dashDoc: Doc | undefined;
- _reactionDisposer: IReactionDisposer | undefined;
- _renderDisposer: IReactionDisposer | undefined;
- _textBox: FormattedTextBox;
-
- getDocTransform = () => {
- 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![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;
- this._dashSpan = document.createElement("div");
- this._outer = document.createElement("span");
- this._outer.style.position = "relative";
- this._outer.style.textIndent = "0";
- this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
- this._outer.style.width = node.attrs.width;
- this._outer.style.height = node.attrs.height;
- this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
- // this._outer.style.overflow = "hidden"; // bcz: not sure if this is needed. if it's used, then the doc doesn't highlight when you hover over a docComment
- (this._outer.style as any).float = node.attrs.float;
-
- this._dashSpan.style.width = node.attrs.width;
- this._dashSpan.style.height = node.attrs.height;
- this._dashSpan.style.position = "absolute";
- this._dashSpan.style.display = "inline-block";
- this._dashSpan.onpointerleave = () => {
- const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
- if (ele) {
- (ele as HTMLDivElement).style.backgroundColor = "";
- }
- };
- this._dashSpan.onpointerenter = () => {
- const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
- if (ele) {
- (ele as HTMLDivElement).style.backgroundColor = "orange";
- }
- };
- 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.Document[Id];// 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.MakeAlias(dashDocBase, docid + alias);
- aliasedDoc.layoutKey = "layout";
- node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
- self.doRender(aliasedDoc, removeDoc, node, view, getPos);
- }
- });
- } else {
- self.doRender(dashDoc, removeDoc, node, view, getPos);
- }
- });
- const self = this;
- this._dashSpan.onkeydown = function (e: any) {
- e.stopPropagation();
- if (e.key === "Tab" || e.key === "Enter") {
- e.preventDefault();
- }
- };
- this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
- this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
- this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
- 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;
- const self = this;
- const dashLayoutDoc = Doc.Layout(dashDoc);
- const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
- if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
- else {
- this._reactionDisposer?.();
- this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => {
- this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px";
- this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px";
- this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
- }, { fireImmediately: true });
- const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
- ReactDOM.unmountComponentAtNode(this._dashSpan);
- ReactDOM.render(<DocumentView
- Document={finalLayout}
- DataDoc={resolvedDataDoc}
- LibraryPath={this._textBox.props.LibraryPath}
- fitToBox={BoolCast(dashDoc._fitToBox)}
- addDocument={returnFalse}
- rootSelected={this._textBox.props.isSelected}
- removeDocument={removeDoc}
- ScreenToLocalTransform={this.getDocTransform}
- addDocTab={this._textBox.props.addDocTab}
- pinToPres={returnFalse}
- renderDepth={self._textBox.props.renderDepth + 1}
- NativeHeight={returnZero}
- NativeWidth={returnZero}
- PanelWidth={finalLayout[WidthSym]}
- PanelHeight={finalLayout[HeightSym]}
- focus={this.outerFocus}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- dontRegisterView={false}
- ContainingCollectionView={this._textBox.props.ContainingCollectionView}
- ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
- ContentScaling={this.contentScaling}
- />, this._dashSpan);
- 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);
- }
- }
- };
- this._renderDisposer?.();
- this._renderDisposer = reaction(() => {
- // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
- // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here?
- // }
- 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[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
- }
- }
- return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
- },
- (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
- { fireImmediately: true });
- }
- }
- destroy() {
- ReactDOM.unmountComponentAtNode(this._dashSpan);
- this._reactionDisposer?.();
- }
-}
-
-
-export class DashFieldView {
- _fieldWrapper: HTMLDivElement; // container for label and value
- _labelSpan: HTMLSpanElement; // field label
- _fieldSpan: HTMLSpanElement; // field value
- _fieldCheck: HTMLInputElement;
- _enumerables: HTMLDivElement; // field value
- _reactionDisposer: IReactionDisposer | undefined;
- _textBoxDoc: Doc;
- @observable _dashDoc: Doc | undefined;
- _fieldKey: string;
- _options: Doc[] = [];
-
- constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- this._fieldKey = node.attrs.fieldKey;
- this._textBoxDoc = tbox.props.Document;
- this._fieldWrapper = document.createElement("p");
- this._fieldWrapper.style.width = node.attrs.width;
- this._fieldWrapper.style.height = node.attrs.height;
- this._fieldWrapper.style.fontWeight = "bold";
- this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.display = "inline-block";
-
- const self = this;
- this._enumerables = document.createElement("div");
- this._enumerables.style.width = "10px";
- this._enumerables.style.height = "10px";
- this._enumerables.style.position = "relative";
- this._enumerables.style.display = "none";
-
- this._enumerables.onpointerdown = async (e) => {
- e.stopPropagation();
- const collview = await Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
- collview instanceof Doc && tbox.props.addDocTab(collview, "onRight");
- };
- const updateText = (forceMatch: boolean) => {
- self._enumerables.style.display = "none";
- const newText = self._fieldSpan.innerText.startsWith(":=") || self._fieldSpan.innerText.startsWith("=:=") ? ":=-computed-" : self._fieldSpan.innerText;
-
- // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
- // holds the different enumerated values for the field in the titles of its collected documents.
- // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
- DocServer.GetRefField(self._fieldKey).then(options => {
- let modText = "";
- (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
- if (modText) {
- self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = modText;
- Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, []);
- } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
- else if (self._fieldSpan.innerText.startsWith(":=")) {
- self._dashDoc![self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
- } else if (self._fieldSpan.innerText.startsWith("=:=")) {
- Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(3));
- } else {
- self._dashDoc![self._fieldKey] = newText;
- }
- });
- };
-
-
- this._fieldCheck = document.createElement("input");
- this._fieldCheck.id = Utils.GenerateGuid();
- this._fieldCheck.type = "checkbox";
- this._fieldCheck.style.position = "relative";
- this._fieldCheck.style.display = "none";
- this._fieldCheck.style.minWidth = "12px";
- this._fieldCheck.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
- this._fieldCheck.onchange = function (e: any) {
- self._dashDoc![self._fieldKey] = e.target.checked;
- };
-
- this._fieldSpan = document.createElement("span");
- this._fieldSpan.id = Utils.GenerateGuid();
- this._fieldSpan.contentEditable = "true";
- this._fieldSpan.style.position = "relative";
- this._fieldSpan.style.display = "none";
- this._fieldSpan.style.minWidth = "12px";
- this._fieldSpan.style.fontSize = "large";
- this._fieldSpan.onkeypress = function (e: any) { e.stopPropagation(); };
- this._fieldSpan.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); self._enumerables.style.display = "inline-block"; };
- this._fieldSpan.onblur = function (e: any) { updateText(false); };
-
- const setDashDoc = (doc: Doc) => {
- self._dashDoc = doc;
- if (self._options?.length && !self._dashDoc[self._fieldKey]) {
- self._dashDoc[self._fieldKey] = StrCast(self._options[0].title);
- }
- this._labelSpan.innerHTML = `${self._fieldKey}: `;
- const fieldVal = Cast(this._dashDoc?.[self._fieldKey], "boolean", null);
- this._fieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
- this._fieldSpan.style.display = !(fieldVal === true || fieldVal === false) ? StrCast(this._dashDoc?.[self._fieldKey]) ? "" : "inline-block" : "none";
- };
- this._fieldSpan.onkeydown = function (e: any) {
- e.stopPropagation();
- if ((e.key === "a" && e.ctrlKey) || (e.key === "a" && e.metaKey)) {
- if (window.getSelection) {
- const range = document.createRange();
- range.selectNodeContents(self._fieldSpan);
- window.getSelection()!.removeAllRanges();
- window.getSelection()!.addRange(range);
- }
- e.preventDefault();
- }
- if (e.key === "Enter") {
- e.preventDefault();
- e.ctrlKey && Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
- updateText(true);
- }
- };
-
- this._labelSpan = document.createElement("span");
- this._labelSpan.style.position = "relative";
- this._labelSpan.style.fontSize = "small";
- this._labelSpan.title = "click to see related tags";
- this._labelSpan.style.fontSize = "x-small";
- this._labelSpan.onpointerdown = function (e: any) {
- e.stopPropagation();
- let container = tbox.props.ContainingCollectionView;
- while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
- container = container.props.ContainingCollectionView;
- }
- if (container) {
- const alias = Doc.MakeAlias(container.props.Document);
- alias.viewType = CollectionViewType.Time;
- let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
- if (!list) {
- alias.schemaColumns = list = new List<SchemaHeaderField>();
- }
- list.map(c => c.heading).indexOf(self._fieldKey) === -1 && list.push(new SchemaHeaderField(self._fieldKey, "#f1efeb"));
- list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
- alias._pivotField = self._fieldKey;
- tbox.props.addDocTab(alias, "onRight");
- }
- };
- this._labelSpan.innerHTML = `${self._fieldKey}: `;
- if (node.attrs.docid) {
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => setDashDoc(dashDoc)));
- } else {
- setDashDoc(tbox.props.DataDoc || tbox.dataDoc);
- }
- this._reactionDisposer?.();
- this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes
- const dashVal = this._dashDoc?.[self._fieldKey];
- return StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(tbox.props.Document)[self._fieldKey] : dashVal;
- }, fval => {
- const boolVal = Cast(fval, "boolean", null);
- if (boolVal === true || boolVal === false) {
- this._fieldCheck.checked = boolVal;
- } else {
- this._fieldSpan.innerHTML = Field.toString(fval as Field) || "";
- }
- this._fieldCheck.style.display = (boolVal === true || boolVal === false) ? "inline-block" : "none";
- this._fieldSpan.style.display = !(fval === true || fval === false) ? (StrCast(fval) ? "" : "inline-block") : "none";
- }, { fireImmediately: true });
-
- this._fieldWrapper.appendChild(this._labelSpan);
- this._fieldWrapper.appendChild(this._fieldCheck);
- this._fieldWrapper.appendChild(this._fieldSpan);
- this._fieldWrapper.appendChild(this._enumerables);
- (this as any).dom = this._fieldWrapper;
- //updateText(false);
- }
- destroy() {
- this._reactionDisposer?.();
- }
- selectNode() { }
-}
-
-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
- }
-}
-
-export class FootnoteView {
- innerView: any;
- outerView: any;
- node: any;
- dom: any;
- getPos: any;
-
- constructor(node: any, view: any, getPos: any) {
- // We'll need these later
- this.node = node;
- this.outerView = view;
- this.getPos = getPos;
-
- // The node's representation in the editor (empty, for now)
- this.dom = document.createElement("footnote");
- this.dom.addEventListener("pointerup", this.toggle, true);
- // These are used when the footnote is selected
- this.innerView = null;
- }
- selectNode() {
- const attrs = { ...this.node.attrs };
- attrs.visibility = true;
- this.dom.classList.add("ProseMirror-selectednode");
- if (!this.innerView) this.open();
- }
-
- deselectNode() {
- const attrs = { ...this.node.attrs };
- attrs.visibility = false;
- this.dom.classList.remove("ProseMirror-selectednode");
- if (this.innerView) this.close();
- }
- open() {
- // Append a tooltip to the outer node
- const tooltip = this.dom.appendChild(document.createElement("div"));
- tooltip.className = "footnote-tooltip";
- // And put a sub-ProseMirror into that
- this.innerView = new EditorView(tooltip, {
- // You can use any node as an editor document
- state: EditorState.create({
- doc: this.node,
- plugins: [keymap(baseKeymap),
- keymap({
- "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
- "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
- "Mod-b": toggleMark(schema.marks.strong)
- }),
- // new Plugin({
- // view(newView) {
- // // TODO -- make this work with RichTextMenu
- // // return FormattedTextBox.getToolTip(newView);
- // }
- // })
- ],
-
- }),
- // This is the magic part
- dispatchTransaction: this.dispatchInner.bind(this),
- handleDOMEvents: {
- pointerdown: ((view: any, e: PointerEvent) => {
- // Kludge to prevent issues due to the fact that the whole
- // footnote is node-selected (and thus DOM-selected) when
- // the parent editor is focused.
- e.stopPropagation();
- document.addEventListener("pointerup", this.ignore, true);
- if (this.outerView.hasFocus()) this.innerView.focus();
- }) as any
- }
-
- });
- setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
- }
-
- ignore = (e: PointerEvent) => {
- e.stopPropagation();
- document.removeEventListener("pointerup", this.ignore, true);
- }
-
- toggle = () => {
- if (this.innerView) this.close();
- else {
- this.open();
- }
- }
- close() {
- this.innerView && this.innerView.destroy();
- this.innerView = null;
- this.dom.textContent = "";
- }
- dispatchInner(tr: any) {
- const { state, transactions } = this.innerView.state.applyTransaction(tr);
- this.innerView.updateState(state);
-
- if (!tr.getMeta("fromOutside")) {
- const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
- for (const transaction of transactions) {
- const steps = transaction.steps;
- for (const step of steps) {
- outerTr.step(step.map(offsetMap));
- }
- }
- if (outerTr.docChanged) this.outerView.dispatch(outerTr);
- }
- }
- update(node: any) {
- if (!node.sameMarkup(this.node)) return false;
- this.node = node;
- if (this.innerView) {
- const state = this.innerView.state;
- const start = node.content.findDiffStart(state.doc.content);
- if (start !== null) {
- let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
- const overlap = start - Math.min(endA, endB);
- if (overlap > 0) { endA += overlap; endB += overlap; }
- this.innerView.dispatch(
- state.tr
- .replace(start, endB, node.slice(start, endA))
- .setMeta("fromOutside", true));
- }
- }
- return true;
- }
-
- destroy() {
- if (this.innerView) this.close();
- }
-
- stopEvent(event: any) {
- return this.innerView && this.innerView.dom.contains(event.target);
- }
-
- ignoreMutation() { return true; }
-}
-
-export class SummaryView {
- _collapsed: HTMLElement;
- _view: any;
- constructor(node: any, view: any, getPos: any) {
- this._collapsed = document.createElement("span");
- this._collapsed.className = this.className(node.attrs.visibility);
- this._view = view;
- const js = node.toJSON;
- node.toJSON = function () {
- return js.apply(this, arguments);
- };
-
- this._collapsed.onpointerdown = (e: any) => {
- const visible = !node.attrs.visibility;
- const attrs = { ...node.attrs, visibility: visible };
- let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
- if (!visible) { // update summarized text and save in attrs
- textSelection = this.updateSummarizedText(getPos() + 1);
- attrs.text = textSelection.content();
- attrs.textslice = attrs.text.toJSON();
- }
- 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
- e.preventDefault();
- e.stopPropagation();
- this._collapsed.className = this.className(visible);
- };
- (this as any).dom = this._collapsed;
- }
- selectNode() { }
-
- deselectNode() { }
-
- className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
-
- updateSummarizedText(start?: any) {
- const mtype = this._view.state.schema.marks.summarize;
- const mtypeInc = this._view.state.schema.marks.summarizeInclusive;
- let endPos = start;
-
- const visited = new Set();
- for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
- let skip = false;
- this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
- if (node.isLeaf && !visited.has(node) && !skip) {
- if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
- visited.add(node);
- endPos = i + node.nodeSize - 1;
- }
- else skip = true;
- }
- });
- }
- return TextSelection.create(this._view.state.doc, start, endPos);
- }
-}
-// :: Schema
-// This schema rougly corresponds to the document schema used by
-// [CommonMark](http://commonmark.org/), minus the list elements,
-// which are defined in the [`prosemirror-schema-list`](#schema-list)
-// module.
-//
-// To reuse elements from this schema, extend or read from its
-// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
-export const schema = new Schema({ nodes, marks });
-
-const fromJson = schema.nodeFromJSON;
-
-schema.nodeFromJSON = (json: any) => {
- const node = fromJson(json);
- if (json.type === schema.nodes.summary.name) {
- node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
- }
- return node;
-}; \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a49977c42..d509168b6 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -8,10 +8,8 @@ export namespace SelectionManager {
class Manager {
- @observable IsDragging: boolean = false;
SelectedDocuments: ObservableMap<DocumentView, boolean> = new ObservableMap();
-
@action
SelectDoc(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
@@ -78,9 +76,6 @@ export namespace SelectionManager {
if (found) manager.SelectDoc(found, false);
}
- export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
- export function GetIsDragging() { return manager.IsDragging; }
-
export function SelectedDocuments(): Array<DocumentView> {
return Array.from(manager.SelectedDocuments.keys());
}
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 30938688d..1bf242d93 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -7,6 +7,7 @@
box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
flex-direction: column;
background: whitesmoke;
+ padding-top: 10px;
padding-bottom: 10px;
border-radius: 15px;
border: solid #BBBBBBBB 1px;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 0a8f0c9a7..11866aebe 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,7 +1,7 @@
import { Doc, Opt, DataSym } from '../../new_fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
-import { Cast, BoolCast } from '../../new_fields/Types';
+import { Cast, BoolCast, ScriptCast } from '../../new_fields/Types';
import { listSpec } from '../../new_fields/Schema';
import { InkingControl } from './InkingControl';
import { InkTool } from '../../new_fields/InkField';
@@ -11,7 +11,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
interface DocComponentProps {
Document: Doc;
- LayoutDoc?: () => Opt<Doc>;
+ LayoutTemplate?: () => Opt<Doc>;
}
export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
@@ -20,7 +20,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
// This is the "The Document" -- it encapsulates, data, layout, and any templates
@computed get rootDoc() { return Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document; }
// This is the rendering data of a document -- it may be "The Document", or it may be some template document that holds the rendering info
- @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+ @computed get layoutDoc() { return Doc.Layout(this.props.Document, this.props.LayoutTemplate?.()); }
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() { return this.props.Document[DataSym] as Doc; }
@@ -33,6 +33,7 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
interface ViewBoxBaseProps {
Document: Doc;
DataDoc?: Doc;
+ ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
isSelected: (outsideReaction?: boolean) => boolean;
renderDepth: number;
@@ -53,6 +54,8 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
+ lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
+
active = (outsideReaction?: boolean) => !this.props.Document.isBackground && (this.props.rootSelected(outsideReaction) || this.props.isSelected(outsideReaction) || this.props.renderDepth === 0 || this.layoutDoc.forceActive);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
@@ -87,6 +90,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
+ lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
+
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
_annotationKey: string = "annotations";
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c02f79187..3624cdb6d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -15,7 +15,7 @@ import './collections/ParentDocumentSelector.scss';
import './DocumentButtonBar.scss';
import { LinkMenu } from "./linking/LinkMenu";
import { DocumentView } from './nodes/DocumentView';
-import { GoogleRef } from "./nodes/FormattedTextBox";
+import { GoogleRef } from "./nodes/formattedText/FormattedTextBox";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 28cf9fd47..4f34eb9e3 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -21,9 +21,13 @@ $linkGap : 3px;
background: none;
}
+
.documentDecorations-resizer {
pointer-events: auto;
background: $alt-accent;
+ opacity: 0.1;
+ }
+ .documentDecorations-resizer:hover {
opacity: 1;
}
@@ -40,7 +44,6 @@ $linkGap : 3px;
.documentDecorations-radius {
pointer-events: auto;
- background: black;
opacity: 1;
transform: translate(10px, 10px);
grid-row: 4;
@@ -80,7 +83,20 @@ $linkGap : 3px;
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
- background: dimGray;
+ background: unset;
+ opacity: 1;
+ }
+ #documentDecorations-topLeftResizer {
+ border-left: 2px solid;
+ border-top: solid 2px;
+ }
+ #documentDecorations-bottomRightResizer {
+ border-right: 2px solid;
+ border-bottom: solid 2px;
+ }
+ #documentDecorations-topLeftResizer:hover,
+ #documentDecorations-bottomRightResizer:hover {
+ opacity: 1;
}
#documentDecorations-bottomRightResizer {
@@ -90,7 +106,22 @@ $linkGap : 3px;
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
+ background: unset;
+ opacity: 1;
+ }
+ #documentDecorations-topRightResizer {
+ border-right: 2px solid;
+ border-top: 2px solid;
+ }
+ #documentDecorations-bottomLeftResizer {
+ border-left: 2px solid;
+ border-bottom: 2px solid;
+ }
+ #documentDecorations-topRightResizer:hover,
+ #documentDecorations-bottomLeftResizer:hover {
+ cursor: nesw-resize;
background: dimGray;
+ opacity: 1;
}
#documentDecorations-topResizer,
@@ -104,7 +135,6 @@ $linkGap : 3px;
}
.documentDecorations-contextMenu {
- background: $alt-accent;
width: 25px;
height: calc(100% + 8px); // 8px for the height of the top resizer bar
grid-column-start: 1;
@@ -112,14 +142,14 @@ $linkGap : 3px;
pointer-events: all;
}
.documentDecorations-title {
- background: $alt-accent;
opacity: 1;
grid-column-start: 3;
grid-column-end: 4;
pointer-events: auto;
overflow: hidden;
text-align: center;
- display:flex;
+ display: flex;
+ border-bottom: solid 1px;
}
.publishBox {
width: 20px;
@@ -136,7 +166,6 @@ $linkGap : 3px;
.documentDecorations-closeButton {
- background: $alt-accent;
opacity: 1;
grid-column-start: 4;
grid-column-end: 6;
@@ -146,7 +175,6 @@ $linkGap : 3px;
}
.documentDecorations-minimizeButton {
- background: $alt-accent;
opacity: 1;
grid-column-start: 1;
grid-column-end: 3;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 312acd5b2..f8e77d90a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DataSym, Field } from "../../new_fields/Doc";
-import { PositionDocument } from '../../new_fields/documentSchemas';
+import { Document } from '../../new_fields/documentSchemas';
import { ScriptField } from '../../new_fields/ScriptField';
import { Cast, StrCast, NumCast } from "../../new_fields/Types";
import { Utils, setupMoveUpEvents, emptyFunction, returnFalse, simulateMouseClick } from "../../Utils";
@@ -20,6 +20,7 @@ import React = require("react");
import { Id } from '../../new_fields/FieldSymbols';
import e = require('express');
import { CollectionDockingView } from './collections/CollectionDockingView';
+import { MainView } from './MainView';
library.add(faCaretUp);
library.add(faObjectGroup);
@@ -46,6 +47,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
private _resizeUndo?: UndoManager.Batch;
+ private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border
+ private _snapX = 0; _snapY = 0; // last snapped location of resize border
@observable private _accumulatedTitle = "";
@observable private _titleControlString: string = "#title";
@observable private _edtingTitle = false;
@@ -238,12 +241,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { });
if (e.button === 0) {
this._resizeHdlId = e.currentTarget.id;
+ const bounds = e.currentTarget.getBoundingClientRect();
+ this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX;
+ this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY;
this.Interacting = true;
this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
+ SelectionManager.SelectedDocuments()[0].props.setupDragLines?.();
}
+ this._snapX = e.pageX;
+ this._snapY = e.pageY;
}
onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
+ const { thisX, thisY } = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY);
+ move[0] = thisX - this._snapX;
+ move[1] = thisY - this._snapY;
+ this._snapX = thisX;
+ this._snapY = thisY;
+
let dX = 0, dY = 0, dW = 0, dH = 0;
switch (this._resizeHdlId) {
@@ -286,12 +301,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => {
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));
+ const doc = Document(element.rootDoc);
+ let nwidth = doc._nativeWidth || 0;
+ let nheight = doc._nativeHeight || 0;
+ const width = (doc._width || 0);
+ const height = (doc._height || (nheight / nwidth * width));
const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
if (nwidth && nheight) {
if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
@@ -303,8 +317,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.y = (doc.y || 0) + dY * (actualdH - height);
const fixedAspect = (nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
- layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
- layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
+ doc._nativeWidth = nwidth = doc._width || 0;
+ doc._nativeHeight = nheight = doc._height || 0;
}
const anno = Cast(doc.annotationOn, Doc, null);
if (e.ctrlKey && anno) {
@@ -320,24 +334,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
else if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
+ doc._nativeWidth = actualdW / (doc._width || 1) * (doc._nativeWidth || 0);
}
- layoutDoc._width = actualdW;
- if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
- else layoutDoc._height = actualdH;
+ doc._width = actualdW;
+ if (fixedAspect && !doc._fitWidth) doc._height = nheight / nwidth * doc._width;
+ else doc._height = actualdH;
}
else {
if (!fixedAspect) {
- layoutDoc._nativeHeight = actualdH / (layoutDoc._height || 1) * (doc._nativeHeight || 0);
+ doc._nativeHeight = actualdH / (doc._height || 1) * (doc._nativeHeight || 0);
}
- layoutDoc._height = actualdH;
- if (fixedAspect && !layoutDoc._fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
- else layoutDoc._width = actualdW;
+ doc._height = actualdH;
+ if (fixedAspect && !doc._fitWidth) doc._width = nwidth / nheight * doc._height;
+ else doc._width = actualdW;
}
} else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
+ dW && (doc._width = actualdW);
+ dH && (doc._height = actualdH);
+ dH && doc._autoHeight && (doc._autoHeight = false);
}
}
}));
@@ -350,6 +364,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.Interacting = false;
(e.button === 0) && this._resizeUndo?.end();
this._resizeUndo = undefined;
+ DragManager.Vals.Instance.clearSnapLines();
}
@computed
@@ -388,7 +403,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined;
const bounds = this.Bounds;
const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
- if (SelectionManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ if (DragManager.Vals.Instance.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
const minimal = bounds.r - bounds.x < 100 ? true : false;
@@ -473,10 +488,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : <div id="documentDecorations-levelSelector" className="documentDecorations-selector" title="tap to select containing document"
- onPointerDown={this.onSelectorUp} onContextMenu={(e) => e.preventDefault()}>
- <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
- </div>}
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
+ <div id="documentDecorations-levelSelector" className="documentDecorations-selector"
+ title="tap to select containing document" onPointerDown={this.onSelectorUp} onContextMenu={e => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div>}
<div id="documentDecorations-borderRadius" className="documentDecorations-radius"
onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 1977f2406..4f8f9ed69 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -634,7 +634,7 @@ export default class GestureOverlay extends Touchable {
}
// if we're not drawing in a toolglass try to recognize as gesture
else {
- const result = GestureUtils.GestureRecognizer.Recognize(new Array(points));
+ const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize(new Array(points));
let actionPerformed = false;
if (result && result.Score > 0.7) {
switch (result.Name) {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 172c1864a..70ea955e1 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -8,7 +8,7 @@ import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import GestureOverlay from "./GestureOverlay";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
export class InkingControl {
@observable static Instance: InkingControl;
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 81d427f64..04288a9e1 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -39,7 +39,12 @@
}
}
+.mainView-container {
+ color:dimgray;
+}
+
.mainView-container-dark {
+ color: lightgray;
.lm_goldenlayout {
background: dimgray;
}
@@ -54,7 +59,6 @@
}
.contextMenu-cont, .contextMenu-item {
background: dimGray;
- color: lightgray;
}
.contextMenu-item:hover {
background: gray;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8fb67c435..71c2bf245 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faTerminal, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons';
+import { faTerminal, faFile as fileSolid, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, faThumbtack, faTree, faTv, faUndoAlt, faVideo } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -19,7 +19,7 @@ import { DocServer } from '../DocServer';
import { Docs, DocumentOptions } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { HistoryUtil } from '../util/History';
-import RichTextMenu from '../util/RichTextMenu';
+import RichTextMenu from './nodes/formattedText/RichTextMenu';
import { Scripting } from '../util/Scripting';
import SettingsManager from '../util/SettingsManager';
import SharingManager from '../util/SharingManager';
@@ -42,6 +42,8 @@ import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { ScriptField } from '../../new_fields/ScriptField';
+import { TimelineMenu } from './animationtimeline/TimelineMenu';
+import { DragManager } from '../util/DragManager';
@observer
export class MainView extends React.Component {
@@ -103,6 +105,11 @@ export class MainView extends React.Component {
}
library.add(faTerminal);
+ library.add(faLocationArrow);
+ library.add(faSearch);
+ library.add(fileSolid);
+ library.add(faFileDownload);
+ library.add(faStop);
library.add(faCalculator);
library.add(faWindowMaximize);
library.add(faFileAlt);
@@ -141,6 +148,8 @@ export class MainView extends React.Component {
library.add(faCaretUp);
library.add(faFilter);
library.add(faBullseye);
+ library.add(faArrowLeft);
+ library.add(faArrowRight);
library.add(faArrowDown);
library.add(faArrowUp);
library.add(faCloudUploadAlt);
@@ -163,6 +172,9 @@ export class MainView extends React.Component {
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
}
+ if (targets && (targets.length && targets[0].className.toString() !== "timeline-menu-desc" && targets[0].className.toString() !== "timeline-menu-item" && targets[0].className.toString() !== "timeline-menu-input")) {
+ TimelineMenu.Instance.closeMenu();
+ }
});
globalPointerUp = () => this.isPointerDown = false;
@@ -580,6 +592,14 @@ export class MainView extends React.Component {
<MarqueeOptionsMenu />
<RichTextMenu />
<OverlayView />
+ {// TO VIEW SNAP LINES
+ <div className="snapLines" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
+ <svg style={{ width: "100%", height: "100%" }}>
+ {DragManager.Vals.Instance.horizSnapLines.map((l: any) => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
+ {DragManager.Vals.Instance.vertSnapLines.map((l: any) => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
+ </svg>
+ </div>}
+ <TimelineMenu />
</div >);
}
}
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 5776cf070..28de0b7a5 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -9,9 +9,10 @@
}
.metadataEntry-autoSuggester {
- width: 100%;
+ width: 80%;
height: 100%;
- padding-right: 10px;
+ margin: 0;
+ display: inline-block;
}
#metadataEntry-outer {
@@ -25,7 +26,7 @@
flex-direction: column;
}
.metadataEntry-inputArea {
- display:flex;
+ display:inline-block;
flex-direction: row;
}
@@ -44,7 +45,7 @@
.react-autosuggest__input {
border: 1px solid #aaa;
border-radius: 4px;
- width: 100%;
+ width: 75%;
}
.react-autosuggest__input--focused {
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 153b81876..66d3b937e 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -81,9 +81,9 @@ 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, contextParams?: { [name: string]: string }) {
+ public static EditButtonScript(title: string, doc: Doc, fieldKey: string, clientX: number, clientY: number, contextParams?: { [name: string]: string }, defaultScript?: ScriptField) {
let overlayDisposer: () => void = emptyFunction;
- const script = ScriptCast(doc[fieldKey]);
+ const script = ScriptCast(doc[fieldKey]) || defaultScript;
let originalText: string | undefined = undefined;
if (script) {
originalText = script.script.originalScript;
diff --git a/src/client/views/SearchDocBox.tsx b/src/client/views/SearchDocBox.tsx
index 799fa9d85..7bd689b19 100644
--- a/src/client/views/SearchDocBox.tsx
+++ b/src/client/views/SearchDocBox.tsx
@@ -6,7 +6,7 @@ import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
-import { returnFalse } from "../../Utils";
+import { returnFalse, returnZero } from "../../Utils";
import { Docs } from "../documents/Documents";
import { SearchUtil } from "../util/SearchUtil";
import { EditableView } from "./EditableView";
@@ -399,7 +399,13 @@ export class SearchDocBox extends React.Component<FieldViewProps> {
<ContentFittingDocumentView {...this.props}
Document={this.content}
rootSelected={returnFalse}
- getTransform={this.props.ScreenToLocalTransform}>
+ bringToFront={returnFalse}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ parentActive={this.props.active}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}>
</ContentFittingDocumentView>
<div
style={{
diff --git a/src/client/views/animationtimeline/Keyframe.scss b/src/client/views/animationtimeline/Keyframe.scss
new file mode 100644
index 000000000..84c8de287
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.scss
@@ -0,0 +1,105 @@
+@import "./../globalCssVariables.scss";
+
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.bar {
+ height: 100%;
+ width: 5px;
+ position: absolute;
+
+ // pointer-events: none;
+ .menubox {
+ width: 200px;
+ height: 200px;
+ top: 50%;
+ position: relative;
+ background-color: $light-color;
+
+ .menutable {
+ tr:nth-child(odd) {
+ background-color: $light-color-secondary;
+ }
+ }
+ }
+
+ .leftResize {
+ left: -10px;
+ border: 3px solid black;
+ }
+
+ .rightResize {
+ right: -10px;
+ border: 3px solid black;
+ }
+
+ .keyframe-indicator {
+ height: 20px;
+ width: 20px;
+ top: calc(50% - 10px);
+ background-color: white;
+ -ms-transform: rotate(45deg);
+ -webkit-transform: rotate(45deg);
+ transform: rotate(45deg);
+ z-index: 1000;
+ position: absolute;
+ }
+
+ .keyframe-information {
+ display: none;
+ position: relative;
+ // z-index: 100000;
+ // background: $timelineDark;
+ width: 100px;
+ // left: -50px;
+ height: 100px;
+ // top: 40px;
+ }
+
+ .keyframeCircle {
+ left: -10px;
+ border: 3px solid $timelineDark;
+ }
+
+ .fadeLeft {
+ left: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to left, $timelineColor 10%, $light-color);
+ }
+
+ .fadeRight {
+ right: 0px;
+ height: 100%;
+ position: absolute;
+ pointer-events: none;
+ background: linear-gradient(to right, $timelineColor 10%, $light-color);
+ }
+
+ .divider {
+ height: 100%;
+ width: 1px;
+ position: absolute;
+ background-color: black;
+ cursor: col-resize;
+ pointer-events: none;
+ }
+
+ .keyframe {
+ height: 100%;
+ position: absolute;
+ }
+
+ .fadeIn-container,
+ .fadeOut-container,
+ .body-container {
+ position: absolute;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
new file mode 100644
index 000000000..bbd7b2676
--- /dev/null
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -0,0 +1,560 @@
+import * as React from "react";
+import "./Keyframe.scss";
+import "./Timeline.scss";
+import "../globalCssVariables.scss";
+import { observer } from "mobx-react";
+import { observable, reaction, action, IReactionDisposer, observe, computed, runInAction, trace } from "mobx";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { Transform } from "../../util/Transform";
+import { TimelineMenu } from "./TimelineMenu";
+import { Docs } from "../../documents/Documents";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { emptyPath } from "../../../Utils";
+
+
+
+/**
+ * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
+ */
+export namespace KeyframeFunc {
+
+ export enum KeyframeType {
+ end = "end",
+ fade = "fade",
+ default = "default",
+ }
+
+ export enum Direction {
+ left = "left",
+ right = "right"
+ }
+
+ export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => {
+ let leftMost: (RegionData | undefined) = undefined;
+ let rightMost: (RegionData | undefined) = undefined;
+ regions.forEach(region => {
+ const neighbor = RegionData(region);
+ if (currentRegion.position! > neighbor.position) {
+ if (!leftMost || neighbor.position > leftMost.position) {
+ leftMost = neighbor;
+ }
+ } else if (currentRegion.position! < neighbor.position) {
+ if (!rightMost || neighbor.position < rightMost.position) {
+ rightMost = neighbor;
+ }
+ }
+ });
+ if (dir === Direction.left) {
+ return leftMost;
+ } else if (dir === Direction.right) {
+ return rightMost;
+ }
+ };
+
+ export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+ let leftKf: Opt<Doc>;
+ let time: number = 0;
+ const keyframes = DocListCast(region.keyframes!);
+ keyframes.map((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
+ leftKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return leftKf;
+ };
+
+
+ export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right
+ let rightKf: Opt<Doc>;
+ let time: number = Infinity;
+ DocListCast(region.keyframes!).forEach((kf) => {
+ let compTime = currentBarX;
+ if (ref) compTime = NumCast(ref.time);
+ if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
+ rightKf = kf;
+ time = NumCast(kf.time);
+ }
+ });
+ return rightKf;
+ };
+
+ export const defaultKeyframe = () => {
+ const regiondata = new Doc(); //creating regiondata in MILI
+ regiondata.duration = 4000;
+ regiondata.position = 0;
+ regiondata.fadeIn = 1000;
+ regiondata.fadeOut = 1000;
+ regiondata.functions = new List<Doc>();
+ regiondata.hasData = false;
+ return regiondata;
+ };
+
+
+ export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
+ const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+ switch (unit) {
+ case "mili":
+ return time;
+ case "sec":
+ return dir === "pixel" ? time / 1000 : time * 1000;
+ case "min":
+ return dir === "pixel" ? time / 60000 : time * 60000;
+ case "hr":
+ return dir === "pixel" ? time / 3600000 : time * 3600000;
+ default:
+ return time;
+ }
+ };
+}
+
+export const RegionDataSchema = createSchema({
+ position: defaultSpec("number", 0),
+ duration: defaultSpec("number", 0),
+ keyframes: listSpec(Doc),
+ fadeIn: defaultSpec("number", 0),
+ fadeOut: defaultSpec("number", 0),
+ functions: listSpec(Doc),
+ hasData: defaultSpec("boolean", false)
+});
+export type RegionData = makeInterface<[typeof RegionDataSchema]>;
+export const RegionData = makeInterface(RegionDataSchema);
+
+interface IProps {
+ node: Doc;
+ RegionData: Doc;
+ collection: Doc;
+ tickSpacing: number;
+ tickIncrement: number;
+ time: number;
+ changeCurrentBarX: (x: number) => void;
+ transform: Transform;
+ makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc;
+}
+
+
+/**
+ *
+ * This class handles the green region stuff
+ * Key facts:
+ *
+ * Structure looks like this
+ *
+ * region as a whole
+ * <------------------------------REGION------------------------------->
+ *
+ * region broken down
+ *
+ * <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void.........
+ * (start) (Fade 2)
+ * (fade 1) (finish)
+ *
+ *
+ * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
+ * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
+ * If you want to know the exact location of the computer, message me.
+ *
+ * @author Andrew Kim
+ */
+@observer
+export class Keyframe extends React.Component<IProps> {
+
+ @observable private _bar = React.createRef<HTMLDivElement>();
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+
+ @computed private get regiondata() { return RegionData(this.props.RegionData); }
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); }
+ @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+
+ constructor(props: any) {
+ super(props);
+ }
+ componentDidMount() {
+ setTimeout(() => { //giving it a temporary 1sec delay...
+ if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
+ const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end);
+ const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
+ const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade);
+ const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end);
+ (fadeIn.key as Doc).opacity = 1;
+ (fadeOut.key as Doc).opacity = 1;
+ (start.key as Doc).opacity = 0.1;
+ (finish.key as Doc).opacity = 0.1;
+ this.forceUpdate(); //not needed, if setTimeout is gone...
+ }, 1000);
+ }
+
+ @action
+ onBarPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this.createKeyframe(clientX);
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.props.changeCurrentBarX(this.pixelPosition + (clientX - this._bar.current!.getBoundingClientRect().left) * this.props.transform.Scale);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onBarPointerMove);
+ document.addEventListener("pointerup", (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onBarPointerMove);
+ });
+ }
+ }
+
+ @action
+ onBarPointerMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0) {
+ this._mouseToggled = true;
+ }
+ const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
+ const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!;
+ const prevX = this.regiondata.position;
+ const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (futureX <= 0) {
+ this.regiondata.position = 0;
+ } else if ((left && left.position + left.duration >= futureX)) {
+ this.regiondata.position = left.position + left.duration;
+ } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+ this.regiondata.position = right.position - this.regiondata.duration;
+ } else {
+ this.regiondata.position = futureX;
+ }
+ const movement = this.regiondata.position - prevX;
+ this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement);
+ }
+
+ @action
+ onResizeLeft = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeLeft);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeLeft);
+ });
+ }
+
+ @action
+ onResizeRight = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onDragResizeRight);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onDragResizeRight);
+ });
+ }
+
+ @action
+ onDragResizeLeft = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
+ if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
+ this.regiondata.position = leftRegion.position + leftRegion.duration;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - (leftRegion.position + leftRegion.duration);
+ } else if (NumCast(this.keyframes[1].time) + offset >= NumCast(this.keyframes[2].time)) {
+ this.regiondata.position = NumCast(this.keyframes[2].time) - this.regiondata.fadeIn;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time) - NumCast(this.keyframes[2].time) + this.regiondata.fadeIn;
+ } else if (NumCast(this.keyframes[0].time) + offset <= 0) {
+ this.regiondata.position = 0;
+ this.regiondata.duration = NumCast(this.keyframes[this.keyframes.length - 1].time);
+ } else {
+ this.regiondata.duration -= offset;
+ this.regiondata.position += offset;
+ }
+ this.keyframes[0].time = this.regiondata.position;
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+
+
+ @action
+ onDragResizeRight = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
+ const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time);
+ if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way
+ this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut;
+ } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) {
+ this.regiondata.duration = rightRegion.position - this.regiondata.position;
+ } else {
+ this.regiondata.duration += offset;
+ }
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ }
+
+
+ @action
+ createKeyframe = async (clientX: number) => {
+ this._mouseToggled = true;
+ const bar = this._bar.current!;
+ const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+ const position = this.regiondata.position;
+ this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default);
+ this.regiondata.hasData = true;
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
+
+ }
+ }
+
+
+ @action
+ moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+ /**
+ * custom keyframe context menu items (when clicking on the keyframe circle)
+ */
+ @action
+ makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Show Data", () => {
+ runInAction(() => {
+ const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
+ CollectionDockingView.AddRightSplit(kvp, emptyPath);
+ });
+ }),
+ TimelineMenu.Instance.addItem("button", "Delete", () => {
+ runInAction(() => {
+ (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+ this.forceUpdate();
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", "Move", (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ const kfIndex: number = this.keyframes.indexOf(kf);
+ if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.keyframes[kfIndex].time = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ });
+ TimelineMenu.Instance.addMenu("Keyframe");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ /**
+ * context menu for region (anywhere on the green region).
+ */
+ @action
+ makeRegionMenu = (kf: Doc, e: MouseEvent) => {
+ TimelineMenu.Instance.addItem("button", "Remove Region", () =>
+ Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+ TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeIn = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.regiondata.fadeOut = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - val;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => {
+ runInAction(() => {
+ const prevPosition = this.regiondata.position;
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.position = parseInt(val, 10);
+ this.updateKeyframes(this.regiondata.position - prevPosition);
+ }
+ });
+ }),
+ TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => {
+ runInAction(() => {
+ let cannotMove: boolean = false;
+ this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ val += this.regiondata.position;
+ if ((val < 0) || (val > pos && val < pos + dur)) {
+ cannotMove = true;
+ }
+ }
+ });
+ if (!cannotMove) {
+ this.regiondata.duration = parseInt(val, 10);
+ this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
+ this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
+ }
+ });
+ }),
+ TimelineMenu.Instance.addMenu("Region");
+ TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
+ }
+
+ @action
+ updateKeyframes = (incr: number, filter: number[] = []) => {
+ this.keyframes.forEach(kf => {
+ if (!filter.includes(this.keyframes.indexOf(kf))) {
+ kf.time = NumCast(kf.time) + incr;
+ }
+ });
+ }
+
+ /**
+ * hovering effect when hovered (hidden div darkens)
+ */
+ @action
+ onContainerOver = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "1";
+ Doc.BrushDoc(this.props.node);
+ }
+
+ /**
+ * hovering effect when hovered out (hidden div becomes invisible)
+ */
+ @action
+ onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const div = ref.current!;
+ div.style.opacity = "0";
+ Doc.UnBrushDoc(this.props.node);
+ }
+
+
+ ///////////////////////UI STUFF /////////////////////////
+
+
+ /**
+ * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades)
+ * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable)
+ */
+ drawKeyframes = () => {
+ const keyframeDivs: JSX.Element[] = [];
+ return DocListCast(this.regiondata.keyframes).map(kf => {
+ if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) {
+ return <>
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ <div className="keyframeCircle keyframe-indicator"
+ onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }}
+ onContextMenu={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.makeKeyframeMenu(kf, e.nativeEvent);
+ }}
+ onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
+ </div>
+ </div>
+ <div className="keyframe-information" />
+ </>;
+ } else {
+ return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider" />
+ </div>;
+ }
+ });
+ }
+
+ /**
+ * drawing the hidden divs that partition different intervals within a region.
+ */
+ @action
+ drawKeyframeDividers = () => {
+ const keyframeDividers: JSX.Element[] = [];
+ DocListCast(this.regiondata.keyframes).forEach(kf => {
+ const index = this.keyframes.indexOf(kf);
+ if (index !== this.keyframes.length - 1) {
+ const right = this.keyframes[index + 1];
+ const bodyRef = React.createRef<HTMLDivElement>();
+ const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ keyframeDividers.push(
+ <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
+ onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }}
+ onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (index !== 0 || index !== this.keyframes.length - 2) {
+ this._mouseToggled = true;
+ }
+ this.makeRegionMenu(kf, e.nativeEvent);
+ }}>
+ </div>
+ );
+ }
+ });
+ return keyframeDividers;
+ }
+
+ /**
+ * rendering that green region
+ */
+ //154, 206, 223
+ render() {
+ trace();
+ console.log(this.props.RegionData.position);
+ console.log(this.regiondata.position);
+ console.log(this.pixelPosition);
+ return (
+ <div className="bar" ref={this._bar} style={{
+ transform: `translate(${this.pixelPosition}px)`,
+ width: `${this.pixelDuration}px`,
+ background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )`
+ }}
+ onPointerDown={this.onBarPointerDown}>
+ <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div>
+ {/* <div className="keyframe-information"></div> */}
+ <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div>
+ {/* <div className="keyframe-information"></div> */}
+ {this.drawKeyframes()}
+ {this.drawKeyframeDividers()}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
new file mode 100644
index 000000000..f90249771
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -0,0 +1,322 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timeline-toolbox {
+ position: absolute;
+ margin: 0px;
+ padding: 0px;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: row;
+ // justify-content: space-evenly;
+ align-items: center;
+ top: 3px;
+ width: 100%;
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ }
+
+ .playbackControls {
+ display: flex;
+ margin-left: 30px;
+ max-width: 84px;
+ width: 84px;
+
+ .timeline-icon {
+ color: $timelineColor;
+ margin-left: 3px;
+ }
+
+ }
+
+ .grid-box {
+ display: flex;
+ // grid-template-columns: [first] 20% [line2] 20% [line3] 60%;
+ width: calc(100% - 150px);
+ // width: 100%;
+ margin-left: 10px;
+
+ .time-box {
+ margin-left: 10px;
+ min-width: 140px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .resetView-tool {
+ width: 30px;
+ height: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: 3px;
+ color: $timelineDark;
+ }
+
+ .resetView-tool:hover {
+ -webkit-transform: scale(1.1);
+ -ms-transform: scale(1.1);
+ transform: scale(1.1);
+ transition: .2s ease;
+ }
+ }
+
+ .mode-box {
+ display: flex;
+ margin-left: 5px;
+ }
+
+ .overview-box {
+ width: 80%;
+ display: flex;
+ }
+
+ div {
+ padding: 0px;
+ // margin-left: 10px;
+ }
+ }
+
+ .overview-tool {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .animation-text {
+ // font-size: 16px;
+ height: auto;
+ width: auto;
+ white-space: nowrap;
+ font-size: 14px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ }
+
+ .round-toggle {
+ height: 20px;
+ width: 40px;
+ min-width: 40px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 20px;
+ animation-fill-mode: forwards;
+ animation-duration: 500ms;
+ top: 30px;
+ margin-left: 5px;
+
+ input {
+ position: absolute;
+ opacity: 0;
+ height: 0;
+ width: 0;
+ }
+
+ .round-toggle-slider {
+ height: 17px;
+ width: 17px;
+ background-color: white;
+ border: 2px solid $timelineDark;
+ border-radius: 50%;
+ transition: transform 500ms ease-in-out;
+ margin-left: 0px;
+ // margin-top: 0.5px;
+ }
+ }
+
+}
+
+.time-input {
+ height: 20px;
+ // width: 120px;
+ width: 100%;
+ white-space: nowrap;
+ font-size: 12px;
+ color: black;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-left: 5px;
+ margin-left: 5px;
+}
+
+.tick {
+ height: 100%;
+ width: 2px;
+ background-color: black;
+ color: black;
+}
+
+.number-label {
+ color: black;
+ transform: rotate(-90deg) translate(-15px, 8px);
+ font-size: .85em;
+}
+
+.timeline-container {
+ width: 100%;
+ height: 300px;
+ position: absolute;
+ background-color: $light-color-secondary;
+ border-bottom: 2px solid $timelineDark;
+ transition: transform 500ms ease;
+
+ .info-container {
+ margin-top: 50px;
+ right: 20px;
+ position: absolute;
+ height: calc(100% - 100px);
+ width: calc(100% - 140px);
+ overflow: hidden;
+
+ .scrubberbox {
+ position: absolute;
+ background-color: transparent;
+ height: 30px;
+ width: 100%;
+
+ }
+
+ .scrubber {
+ top: 30px;
+ height: 100%;
+ width: 2px;
+ position: absolute;
+ z-index: 1001;
+ background-color: black;
+ pointer-events: none;
+
+ .scrubberhead {
+ top: -20px;
+ height: 20px;
+ width: 20px;
+ background-color: white;
+ border-radius: 50%;
+ border: 3px solid black;
+ left: -9px;
+ position: absolute;
+ pointer-events: all;
+ }
+ }
+
+ .trackbox {
+ top: 30px;
+ height: calc(100% - 30px);
+ // height: 100%;
+ // height: 100%;
+ width: 100%;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ overflow: hidden;
+ // overflow-y: scroll;
+ background-color: white;
+ position: absolute;
+ // box-shadow: -10px 0px 10px 10px red;
+ }
+
+ }
+
+ .currentTime {
+ // background: red;
+ font-size: 12px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 40px;
+ top: 40px;
+ position: relative;
+ width: 100px;
+ margin-left: 20px;
+ }
+
+ .title-container {
+ // margin-top: 80px;
+ margin-top: 40px;
+ margin-left: 20px;
+ height: calc(100% - 100px - 30px);
+ // height: 100%;
+ width: 100px;
+ background-color: white;
+ overflow: hidden;
+ border-left: 2px solid black;
+ border-top: 2px solid black;
+ border-bottom: 2px solid black;
+ border-right: 2px solid $timelineDark;
+
+ .datapane {
+ top: 0px;
+ width: 100px;
+ height: 30%;
+ border: 1px solid $dark-color;
+ font-size: 12px;
+ line-height: 11px;
+ background-color: $timelineDark;
+ color: white;
+ position: relative;
+ float: left;
+ padding: 3px;
+ border-style: solid;
+ overflow-y: scroll;
+ overflow-x: hidden;
+
+ p {
+ hyphens: auto;
+ }
+
+ }
+ }
+
+ .resize {
+ bottom: 0px;
+ position: absolute;
+ height: 20px;
+ width: 40px;
+ left: calc(50% - 25px);
+ color: $timelineDark;
+ }
+}
+
+
+
+.overview {
+ position: absolute;
+ height: 50px;
+ width: 200px;
+ background-color: black;
+
+ .container {
+ position: absolute;
+ float: left 0px;
+ top: 25%;
+ height: 75%;
+ width: 100%;
+ background-color: grey;
+ }
+}
+
+
+.timeline-checker {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ position: absolute;
+ display: flex;
+ padding: 10px 10px;
+
+ div {
+ height: auto;
+ width: auto;
+ overflow: hidden;
+ margin: 0px 10px;
+ cursor: pointer
+ }
+
+ .check {
+ width: 50px;
+ height: 50px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
new file mode 100644
index 000000000..466cbb867
--- /dev/null
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -0,0 +1,622 @@
+import * as React from "react";
+import "./Timeline.scss";
+import { listSpec } from "../../../new_fields/Schema";
+import { observer } from "mobx-react";
+import { Track } from "./Track";
+import { observable, action, computed, runInAction, IReactionDisposer, reaction, trace } from "mobx";
+import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPlayCircle, faBackward, faForward, faGripLines, faPauseCircle, faEyeSlash, faEye, faCheckCircle, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
+import { ContextMenu } from "../ContextMenu";
+import { TimelineOverview } from "./TimelineOverview";
+import { FieldViewProps } from "../nodes/FieldView";
+import { KeyframeFunc } from "./Keyframe";
+import { Utils } from "../../../Utils";
+
+/**
+ * Timeline class controls most of timeline functions besides individual keyframe and track mechanism. Main functions are
+ * zooming, panning, currentBarX (scrubber movement). Most of the UI stuff is also handled here. You shouldn't really make
+ * any logical changes here. Most work is needed on UI.
+ *
+ * The hierarchy works this way:
+ *
+ * Timeline.tsx --> Track.tsx --> Keyframe.tsx
+ | |
+ | TimelineMenu.tsx (timeline's custom contextmenu)
+ |
+ |
+ TimelineOverview.tsx (youtube like dragging thing is play mode, complex dragging thing in editing mode)
+
+
+ Most style changes are in SCSS file.
+ If you have any questions, email me or text me.
+ @author Andrew Kim
+ */
+
+
+@observer
+export class Timeline extends React.Component<FieldViewProps> {
+
+
+ //readonly constants
+ private readonly DEFAULT_TICK_SPACING: number = 50;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private readonly MAX_CONTAINER_HEIGHT: number = 800;
+ private readonly DEFAULT_TICK_INCREMENT: number = 1000;
+
+ //height variables
+ private DEFAULT_CONTAINER_HEIGHT: number = 330;
+ private MIN_CONTAINER_HEIGHT: number = 205;
+
+ //react refs
+ @observable private _trackbox = React.createRef<HTMLDivElement>();
+ @observable private _titleContainer = React.createRef<HTMLDivElement>();
+ @observable private _timelineContainer = React.createRef<HTMLDivElement>();
+ @observable private _infoContainer = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleRef = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleContainerRef = React.createRef<HTMLDivElement>();
+ @observable private _timeInputRef = React.createRef<HTMLInputElement>();
+
+ //boolean vars and instance vars
+ @observable private _currentBarX: number = 0;
+ @observable private _windSpeed: number = 1;
+ @observable private _isPlaying: boolean = false; //scrubber playing
+ @observable private _totalLength: number = 0;
+ @observable private _visibleLength: number = 0;
+ @observable private _visibleStart: number = 0;
+ @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
+ @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
+ @observable private _time = 100000; //DEFAULT
+ @observable private _playButton = faPlayCircle;
+ @observable private _mouseToggled = false;
+ @observable private _doubleClickEnabled = false;
+ @observable private _titleHeight = 0;
+
+ // so a reaction can be made
+ @observable public _isAuthoring = this.props.Document.isATOn;
+
+ /**
+ * collection get method. Basically defines what defines collection's children. These will be tracked in the timeline. Do not edit.
+ */
+ @computed
+ private get children(): List<Doc> {
+ const extendedDocument = ["image", "video", "pdf"].includes(StrCast(this.props.Document.type));
+ if (extendedDocument) {
+ if (this.props.Document.data_ext) {
+ return Cast((Cast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + "-annotations"], Doc) as Doc).annotations, listSpec(Doc)) as List<Doc>;
+ } else {
+ return new List<Doc>();
+ }
+ }
+ return Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)) as List<Doc>;
+ }
+
+ /////////lifecycle functions////////////
+ componentWillMount() {
+ const relativeHeight = window.innerHeight / 20; //sets height to arbitrary size, relative to innerHeight
+ this._titleHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //check if relHeight is less than Maxheight. Else, just set relheight to max
+ this.MIN_CONTAINER_HEIGHT = this._titleHeight + 130; //offset
+ this.DEFAULT_CONTAINER_HEIGHT = this._titleHeight * 2 + 130; //twice the titleheight + offset
+ }
+
+ componentDidMount() {
+ runInAction(() => {
+ if (!this.props.Document.AnimationLength) { //if animation length did not exist
+ this.props.Document.AnimationLength = this._time; //set it to default time
+ } else {
+ this._time = NumCast(this.props.Document.AnimationLength); //else, set time to animationlength stored from before
+ }
+ this._totalLength = this._tickSpacing * (this._time / this._tickIncrement); //the entire length of the timeline div (actual div part itself)
+ this._visibleLength = this._infoContainer.current!.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current!.scrollLeft; //where the div starts
+ this.props.Document.isATOn = !this.props.Document.isATOn; //turns the boolean on, saying AT (animation timeline) is on
+ this.toggleHandle();
+ });
+ }
+
+ componentWillUnmount() {
+ this.props.Document.AnimationLength = this._time; //save animation length
+ }
+ /////////////////////////////////////////////////
+
+ /**
+ * React Functional Component
+ * Purpose: For drawing Tick marks across the timeline in authoring mode
+ */
+ @action
+ drawTicks = () => {
+ const ticks = [];
+ for (let i = 0; i < this._time / this._tickIncrement; i++) {
+ ticks.push(<div key={Utils.GenerateGuid()} className="tick" style={{ transform: `translate(${i * this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p className="number-label">{this.toReadTime(i * this._tickIncrement)}</p></div>);
+ }
+ return ticks;
+ }
+
+ /**
+ * changes the scrubber to actual pixel position
+ */
+ @action
+ changeCurrentBarX = (pixel: number) => {
+ pixel <= 0 ? this._currentBarX = 0 : pixel >= this._totalLength ? this._currentBarX = this._totalLength : this._currentBarX = pixel;
+ }
+
+ //for playing
+ @action
+ onPlay = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.play();
+ }
+
+ /**
+ * when playbutton is clicked
+ */
+ @action
+ play = () => {
+ if (this._isPlaying) {
+ this._isPlaying = false;
+ this._playButton = faPlayCircle;
+ } else {
+ this._isPlaying = true;
+ this._playButton = faPauseCircle;
+ const playTimeline = () => {
+ if (this._isPlaying) {
+ if (this._currentBarX >= this._totalLength) {
+ this.changeCurrentBarX(0);
+ } else {
+ this.changeCurrentBarX(this._currentBarX + this._windSpeed);
+ }
+ setTimeout(playTimeline, 15);
+ }
+ };
+ playTimeline();
+ }
+ }
+
+
+ /**
+ * fast forward the timeline scrubbing
+ */
+ @action
+ windForward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed < 64) { //max speed is 32
+ this._windSpeed = this._windSpeed * 2;
+ }
+ }
+
+ /**
+ * rewind the timeline scrubbing
+ */
+ @action
+ windBackward = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._windSpeed > 1 / 16) { // min speed is 1/8
+ this._windSpeed = this._windSpeed / 2;
+ }
+ }
+
+ /**
+ * scrubber down
+ */
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ });
+ }
+
+ /**
+ * when there is any scrubber movement
+ */
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberbox = this._infoContainer.current!;
+ const left = scrubberbox.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left) * this.props.ScreenToLocalTransform().Scale;
+ this.changeCurrentBarX(offsetX + this._visibleStart); //changes scrubber to clicked scrubber position
+ }
+
+ /**
+ * when panning the timeline (in editing mode)
+ */
+ @action
+ onPanDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const clientX = e.clientX;
+ if (this._doubleClickEnabled) {
+ this._doubleClickEnabled = false;
+ } else {
+ setTimeout(() => {
+ if (!this._mouseToggled && this._doubleClickEnabled) this.changeCurrentBarX(this._trackbox.current!.scrollLeft + clientX - this._trackbox.current!.getBoundingClientRect().left);
+ this._mouseToggled = false;
+ this._doubleClickEnabled = false;
+ }, 200);
+ this._doubleClickEnabled = true;
+ document.addEventListener("pointermove", this.onPanMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onPanMove);
+ if (!this._doubleClickEnabled) {
+ this._mouseToggled = false;
+ }
+ });
+
+ }
+ }
+
+ /**
+ * when moving the timeline (in editing mode)
+ */
+ @action
+ onPanMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.movementX !== 0 || e.movementY !== 0) {
+ this._mouseToggled = true;
+ }
+ const trackbox = this._trackbox.current!;
+ const titleContainer = this._titleContainer.current!;
+ this.movePanX(this._visibleStart - e.movementX);
+ trackbox.scrollTop = trackbox.scrollTop - e.movementY;
+ titleContainer.scrollTop = titleContainer.scrollTop - e.movementY;
+ if (this._visibleStart + this._visibleLength + 20 >= this._totalLength) {
+ this._visibleStart -= e.movementX;
+ this._totalLength -= e.movementX;
+ this._time -= KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ this.props.Document.AnimationLength = this._time;
+ }
+
+ }
+
+
+ @action
+ movePanX = (pixel: number) => {
+ const infoContainer = this._infoContainer.current!;
+ infoContainer.scrollLeft = pixel;
+ this._visibleStart = infoContainer.scrollLeft;
+ }
+
+ /**
+ * resizing timeline (in editing mode) (the hamburger drag icon)
+ */
+ @action
+ onResizeDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.addEventListener("pointermove", this.onResizeMove);
+ document.addEventListener("pointerup", () => {
+ document.removeEventListener("pointermove", this.onResizeMove);
+ });
+ }
+
+ @action
+ onResizeMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientY - this._timelineContainer.current!.getBoundingClientRect().bottom;
+ // let offset = 0;
+ if (this._containerHeight + offset <= this.MIN_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MIN_CONTAINER_HEIGHT;
+ } else if (this._containerHeight + offset >= this.MAX_CONTAINER_HEIGHT) {
+ this._containerHeight = this.MAX_CONTAINER_HEIGHT;
+ } else {
+ this._containerHeight += offset;
+ }
+ }
+
+ /**
+ * for displaying time to standard min:sec
+ */
+ @action
+ toReadTime = (time: number): string => {
+ time = time / 1000;
+ const inSeconds = Math.round(time * 100) / 100;
+
+ const min: (string | number) = Math.floor(inSeconds / 60);
+ const sec: (string | number) = (Math.round((inSeconds % 60) * 100) / 100);
+ let secString = sec.toFixed(2);
+
+ if (Math.floor(sec / 10) === 0) {
+ secString = "0" + secString;
+ }
+
+ return `${min}:${secString}`;
+ }
+
+
+ /**
+ * timeline zoom function
+ * use mouse middle button to zoom in/out the timeline
+ */
+ @action
+ onWheelZoom = (e: React.WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const offset = e.clientX - this._infoContainer.current!.getBoundingClientRect().left;
+ const prevTime = KeyframeFunc.convertPixelTime(this._visibleStart + offset, "mili", "time", this._tickSpacing, this._tickIncrement);
+ const prevCurrent = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ e.deltaY < 0 ? this.zoom(true) : this.zoom(false);
+ const currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ const currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._infoContainer.current!.scrollLeft = currPixel - offset;
+ this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0;
+ this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) : 0;
+ this.changeCurrentBarX(currCurrent);
+ }
+
+
+ resetView(doc: Doc) {
+ doc._panX = doc._customOriginX ?? 0;
+ doc._panY = doc._customOriginY ?? 0;
+ doc.scale = doc._customOriginScale ?? 1;
+ }
+
+ setView(doc: Doc) {
+ doc._customOriginX = doc._panX;
+ doc._customOriginY = doc._panY;
+ doc._customOriginScale = doc.scale;
+ }
+ /**
+ * zooming mechanism (increment and spacing changes)
+ */
+ @action
+ zoom = (dir: boolean) => {
+ let spacingChange = this._tickSpacing;
+ let incrementChange = this._tickIncrement;
+ if (dir) {
+ if (!(this._tickSpacing === 100 && this._tickIncrement === 1000)) {
+ if (this._tickSpacing >= 100) {
+ incrementChange /= 2;
+ spacingChange = 50;
+ } else {
+ spacingChange += 5;
+ }
+ }
+ } else {
+ if (this._tickSpacing <= 50) {
+ spacingChange = 100;
+ incrementChange *= 2;
+ } else {
+ spacingChange -= 5;
+ }
+ }
+ const finalLength = spacingChange * (this._time / incrementChange);
+ if (finalLength >= this._infoContainer.current!.getBoundingClientRect().width) {
+ this._totalLength = finalLength;
+ this._tickSpacing = spacingChange;
+ this._tickIncrement = incrementChange;
+ }
+ }
+
+ /**
+ * tool box includes the toggle buttons at the top of the timeline (both editing mode and play mode)
+ */
+ private timelineToolBox = (scale: number, totalTime: number) => {
+ const size = 40 * scale; //50 is default
+ const iconSize = 25;
+
+ //decides if information should be omitted because the timeline is very small
+ // if its less than 950 pixels then it's going to be overlapping
+ let shouldCompress = false;
+ const width: number = this.props.PanelWidth();
+ if (width < 850) {
+ shouldCompress = true;
+ }
+
+ let modeString, overviewString, lengthString;
+ const modeType = this.props.Document.isATOn ? "Author" : "Play";
+
+ if (!shouldCompress) {
+ modeString = "Mode: " + modeType;
+ overviewString = "Overview:";
+ lengthString = "Length: ";
+ }
+ else {
+ modeString = modeType;
+ overviewString = "";
+ lengthString = "";
+ }
+
+ // let rightInfo = this.timeIndicator;
+
+ return (
+ <div key="timeline_toolbox" className="timeline-toolbox" style={{ height: `${size}px` }}>
+ <div className="playbackControls">
+ <div className="timeline-icon" key="timeline_windBack" onClick={this.windBackward} title="Slow Down Animation"> <FontAwesomeIcon icon={faBackward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key=" timeline_play" onClick={this.onPlay} title="Play/Pause"> <FontAwesomeIcon icon={this._playButton} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ <div className="timeline-icon" key="timeline_windForward" onClick={this.windForward} title="Speed Up Animation"> <FontAwesomeIcon icon={faForward} style={{ height: `${iconSize}px`, width: `${iconSize}px` }} /> </div>
+ </div>
+ <div className="grid-box overview-tool">
+ <div className="overview-box">
+ <div key="overview-text" className="animation-text">{overviewString}</div>
+ <TimelineOverview tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} time={this._time} parent={this} isAuthoring={BoolCast(this.props.Document.isATOn)} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX} />
+ </div>
+ <div className="mode-box overview-tool">
+ <div key="animation-text" className="animation-text">{modeString}</div>
+ <div key="round-toggle" ref={this._roundToggleContainerRef} className="round-toggle">
+ <div key="round-toggle-slider" ref={this._roundToggleRef} className="round-toggle-slider" onPointerDown={this.toggleChecked}> </div>
+ </div>
+ </div>
+ <div className="time-box overview-tool" style={{ display: "flex" }}>
+ {this.timeIndicator(lengthString, totalTime)}
+ <div className="resetView-tool" title="Return to Default View" onClick={() => this.resetView(this.props.Document)}><FontAwesomeIcon icon="compress-arrows-alt" size="lg" /></div>
+ <div className="resetView-tool" style={{ display: this._isAuthoring ? "flex" : "none" }} title="Set Default View" onClick={() => this.setView(this.props.Document)}><FontAwesomeIcon icon="expand-arrows-alt" size="lg" /></div>
+
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ timeIndicator(lengthString: string, totalTime: number) {
+ if (this.props.Document.isATOn) {
+ return (
+ <div key="time-text" className="animation-text" style={{ visibility: this.props.Document.isATOn ? "visible" : "hidden", display: this.props.Document.isATOn ? "flex" : "none" }}>{`Total: ${this.toReadTime(totalTime)}`}</div>
+ );
+ }
+ else {
+ const ctime = `Current: ${this.getCurrentTime()}`;
+ const ttime = `Total: ${this.toReadTime(this._time)}`;
+ return (
+ <div style={{ flexDirection: "column" }}>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ {ctime}
+ </div>
+ <div className="animation-text" style={{ fontSize: "10px", width: "100%", display: !this.props.Document.isATOn ? "block" : "none" }}>
+ {ttime}
+ </div>
+ </div>
+ );
+ }
+ }
+
+ /**
+ * when the user decides to click the toggle button (either user wants to enter editing mode or play mode)
+ */
+ @action
+ private toggleChecked = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.toggleHandle();
+ }
+
+ /**
+ * turns on the toggle button (the purple slide button that changes from editing mode and play mode
+ */
+ private toggleHandle = () => {
+ const roundToggle = this._roundToggleRef.current!;
+ const roundToggleContainer = this._roundToggleContainerRef.current!;
+ const timelineContainer = this._timelineContainer.current!;
+ if (BoolCast(this.props.Document.isATOn)) {
+ //turning on playmode...
+ roundToggle.style.transform = "translate(0px, 0px)";
+ roundToggle.style.animationName = "turnoff";
+ roundToggleContainer.style.animationName = "turnoff";
+ roundToggleContainer.style.backgroundColor = "white";
+ timelineContainer.style.top = `${-this._containerHeight}px`;
+ this.props.Document.isATOn = false;
+ this._isAuthoring = false;
+ this.toPlay();
+ } else {
+ //turning on authoring mode...
+ roundToggle.style.transform = "translate(20px, 0px)";
+ roundToggle.style.animationName = "turnon";
+ roundToggleContainer.style.animationName = "turnon";
+ roundToggleContainer.style.backgroundColor = "#9acedf";
+ timelineContainer.style.top = "0px";
+ this.props.Document.isATOn = true;
+ this._isAuthoring = true;
+ this.toAuthoring();
+ }
+ }
+
+
+ @action.bound
+ changeLengths() {
+ if (this._infoContainer.current) {
+ this._visibleLength = this._infoContainer.current.getBoundingClientRect().width; //the visible length of the timeline (the length that you current see)
+ this._visibleStart = this._infoContainer.current.scrollLeft; //where the div starts
+ }
+ }
+
+ // @computed
+ getCurrentTime = () => {
+ let current = KeyframeFunc.convertPixelTime(this._currentBarX, "mili", "time", this._tickSpacing, this._tickIncrement);
+ if (current > this._time) {
+ current = this._time;
+ }
+ return this.toReadTime(current);
+ }
+
+ @observable private mapOfTracks: (Track | null)[] = [];
+
+ @action
+ findLongestTime = () => {
+ let longestTime: number = 0;
+ this.mapOfTracks.forEach(track => {
+ if (track) {
+ const lastTime = track.getLastRegionTime();
+ if (this.children.length !== 0) {
+ if (longestTime <= lastTime) {
+ longestTime = lastTime;
+ }
+ }
+ } else {
+ //TODO: remove undefineds and duplicates
+ }
+ });
+ // console.log(longestTime);
+ return longestTime;
+ }
+
+ @action
+ toAuthoring = () => {
+ let longestTime = this.findLongestTime();
+ if (longestTime === 0) longestTime = 1;
+ const adjustedTime = Math.ceil(longestTime / 100000) * 100000;
+ // console.log(adjustedTime);
+ this._totalLength = KeyframeFunc.convertPixelTime(adjustedTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ this._time = adjustedTime;
+ }
+
+ @action
+ toPlay = () => {
+ const longestTime = this.findLongestTime();
+ this._time = longestTime;
+ this._totalLength = KeyframeFunc.convertPixelTime(this._time, "mili", "pixel", this._tickSpacing, this._tickIncrement);
+ }
+
+ /**
+ * if you have any question here, just shoot me an email or text.
+ * basically the only thing you need to edit besides render methods in track (individual track lines) and keyframe (green region)
+ */
+ render() {
+ setTimeout(() => {
+ this.changeLengths();
+ // this.toPlay();
+ // this._time = longestTime;
+ }, 0);
+
+ const longestTime = this.findLongestTime();
+ trace();
+ // change visible and total width
+ return (
+ <div style={{ visibility: "visible" }}>
+ <div key="timeline_wrapper" style={{ visibility: BoolCast(this.props.Document.isATOn) ? "visible" : "hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)" }}>
+ <div key="timeline_container" className="timeline-container" ref={this._timelineContainer} style={{ height: `${this._containerHeight}px`, top: `0px` }}>
+ <div key="timeline_info" className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
+ {this.drawTicks()}
+ <div key="timeline_scrubber" className="scrubber" style={{ transform: `translate(${this._currentBarX}px)` }}>
+ <div key="timeline_scrubberhead" className="scrubberhead" onPointerDown={this.onScrubberDown} ></div>
+ </div>
+ <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{ width: `${this._totalLength}px` }}>
+ {DocListCast(this.children).map(doc =>
+ <Track ref={ref => this.mapOfTracks.push(ref)} node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing={this._tickSpacing} tickIncrement={this._tickIncrement} collection={this.props.Document} timelineVisible={true} />
+ )}
+ </div>
+ </div>
+ <div className="currentTime">Current: {this.getCurrentTime()}</div>
+ <div key="timeline_title" className="title-container" ref={this._titleContainer}>
+ {DocListCast(this.children).map(doc => <div style={{ height: `${(this._titleHeight)}px` }} className="datapane" onPointerOver={() => { Doc.BrushDoc(doc); }} onPointerOut={() => { Doc.UnBrushDoc(doc); }}><p>{doc.title}</p></div>)}
+ </div>
+ <div key="timeline_resize" onPointerDown={this.onResizeDown}>
+ <FontAwesomeIcon className="resize" icon={faGripLines} />
+ </div>
+ </div>
+ </div>
+ {this.timelineToolBox(1, longestTime)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.scss b/src/client/views/animationtimeline/TimelineMenu.scss
new file mode 100644
index 000000000..7ee0a43d5
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.scss
@@ -0,0 +1,94 @@
+@import "./../globalCssVariables.scss";
+
+
+.timeline-menu-container{
+ position: absolute;
+ display: flex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+ background: whitesmoke;
+ z-index: 10000;
+ width: 200px;
+ padding-bottom: 10px;
+ border-radius: 15px;
+
+ border: solid #BBBBBBBB 1px;
+
+
+
+ .timeline-menu-input{
+ font: $sans-serif;
+ font-size: 13px;
+ width:100%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ margin-left: 10px;
+ background-color: transparent;
+ border-width: 0px;
+ transition: border-width 500ms;
+ }
+
+ .timeline-menu-input:hover{
+ border-width: 2px;
+ }
+
+
+
+
+ .timeline-menu-header{
+ border-top-left-radius: 15px;
+ border-top-right-radius: 15px;
+ text-transform: uppercase;
+ background: $dark-color;
+ letter-spacing: 2px;
+
+ .timeline-menu-header-desc{
+ font:$sans-serif;
+ font-size: 13px;
+ text-align: center;
+ color: whitesmoke;
+ }
+ }
+
+
+ .timeline-menu-item {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: whitesmoke;
+ display: flex; //comment out to allow search icon to be inline with search text
+ justify-content: left;
+ align-items: center;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+ border-style: none;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 13px;
+ color: grey;
+ letter-spacing: 2px;
+ text-transform: uppercase;
+ padding-right: 20px;
+ padding-left: 10px;
+ }
+
+ .timeline-menu-item:hover {
+ border-width: .11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ border-top-style: solid;
+ background: $darker-alt-accent;
+ }
+
+ .timeline-menu-desc {
+ padding-left: 10px;
+ font:$sans-serif;
+ font-size: 13px;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
new file mode 100644
index 000000000..53ca9acad
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -0,0 +1,77 @@
+import * as React from "react";
+import { observable, action, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import "./TimelineMenu.scss";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons";
+import { Utils } from "../../../Utils";
+
+
+@observer
+export class TimelineMenu extends React.Component {
+ public static Instance: TimelineMenu;
+
+ @observable private _opacity = 0;
+ @observable private _x = 0;
+ @observable private _y = 0;
+ @observable private _currentMenu: JSX.Element[] = [];
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ TimelineMenu.Instance = this;
+ }
+
+ @action
+ openMenu = (x?: number, y?: number) => {
+ this._opacity = 1;
+ x ? this._x = x : this._x = 0;
+ y ? this._y = y : this._y = 0;
+ }
+
+ @action
+ closeMenu = () => {
+ this._opacity = 0;
+ this._currentMenu = [];
+ this._x = -1000000;
+ this._y = -1000000;
+ }
+
+ @action
+ addItem = (type: "input" | "button", title: string, event: (e: any, ...args: any[]) => void) => {
+ if (type === "input") {
+ const inputRef = React.createRef<HTMLInputElement>();
+ let text = "";
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg" /><input className="timeline-menu-input" ref={inputRef} placeholder={title} onChange={(e) => {
+ e.stopPropagation();
+ text = e.target.value;
+ }} onKeyDown={(e) => {
+ if (e.keyCode === 13) {
+ event(text);
+ this.closeMenu();
+ e.stopPropagation();
+ }
+ }} /></div>);
+ } else if (type === "button") {
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine} size="lg" /><p className="timeline-menu-desc" onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ event(e);
+ this.closeMenu();
+ }}>{title}</p></div>);
+ }
+ }
+
+ @action
+ addMenu = (title: string) => {
+ this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
+ }
+
+ render() {
+ return (
+ <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{ opacity: this._opacity, left: this._x, top: this._y }} >
+ {this._currentMenu}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.scss b/src/client/views/animationtimeline/TimelineOverview.scss
new file mode 100644
index 000000000..283163ea7
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.scss
@@ -0,0 +1,107 @@
+@import "./../globalCssVariables.scss";
+
+$timelineColor: #9acedf;
+$timelineDark: #77a1aa;
+
+.timelineOverview-bounding {
+ width: 100%;
+ margin-right: 10px;
+}
+
+.timeline-flex {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+}
+
+.timeline-overview-container {
+ // padding: 0px;
+ margin-right: 5px;
+ // margin: 0px;
+ margin-left: 5px;
+ // width: 300px;
+ width: 100%;
+ height: 25px;
+ background: white;
+ position: relative;
+ border: 2px solid $timelineDark;
+ // width: 100%;
+
+ .timeline-overview-visible {
+ position: absolute;
+ height: 21px;
+ background: $timelineColor;
+ display: inline-block;
+ margin: 0px;
+ padding: 0px;
+ // top: 1px;
+ }
+
+ .timeline-overview-scrubber-container {
+ margin: 0px;
+ padding: 0px;
+ position: absolute;
+ height: 100%;
+ width: 2px;
+ top: 0px;
+ left: 0px;
+ z-index: 1001;
+ background-color: black;
+ display: inline-block;
+
+ .timeline-overview-scrubber-head {
+ padding: 0px;
+ margin: 0px;
+ position: absolute;
+ height: 10px;
+ width: 10px;
+ background-color: white;
+ border-radius: 50%;
+ border: 2px solid black;
+ left: -4px;
+ // top: -30px;
+ top: -10px;
+ }
+ }
+}
+
+
+
+.timeline-play-bar {
+ position: relative;
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: 4px;
+ background-color: $timelineColor;
+ border-radius: 20px;
+ cursor: pointer;
+
+ .timeline-play-head {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: white;
+ border: 3px solid $timelineColor;
+ left: 0px;
+ top: -8px;
+ cursor: pointer;
+ }
+}
+
+.timeline-play-tail {
+ position: absolute;
+ padding: 0px;
+ margin: 0px;
+ height: 4px;
+ width: 0px;
+ z-index: 1000;
+ background-color: $timelineDark;
+ border-radius: 20px;
+ margin-top: -4px;
+ cursor: pointer;
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/TimelineOverview.tsx b/src/client/views/animationtimeline/TimelineOverview.tsx
new file mode 100644
index 000000000..31e248823
--- /dev/null
+++ b/src/client/views/animationtimeline/TimelineOverview.tsx
@@ -0,0 +1,181 @@
+import * as React from "react";
+import { observable, action, computed, runInAction, reaction, IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import "./TimelineOverview.scss";
+import * as $ from 'jquery';
+import { Timeline } from "./Timeline";
+import { Keyframe, KeyframeFunc } from "./Keyframe";
+
+
+interface TimelineOverviewProps {
+ totalLength: number;
+ visibleLength: number;
+ visibleStart: number;
+ currentBarX: number;
+ isAuthoring: boolean;
+ parent: Timeline;
+ changeCurrentBarX: (pixel: number) => void;
+ movePanX: (pixel: number) => any;
+ time: number;
+ tickSpacing: number;
+ tickIncrement: number;
+}
+
+
+@observer
+export class TimelineOverview extends React.Component<TimelineOverviewProps>{
+ @observable private _visibleRef = React.createRef<HTMLDivElement>();
+ @observable private _scrubberRef = React.createRef<HTMLDivElement>();
+ @observable private authoringContainer = React.createRef<HTMLDivElement>();
+ @observable private playbackContainer = React.createRef<HTMLDivElement>();
+ @observable private overviewBarWidth: number = 0;
+ @observable private playbarWidth: number = 0;
+ @observable private activeOverviewWidth: number = 0;
+ @observable private _authoringReaction?: IReactionDisposer;
+ @observable private visibleTime: number = 0;
+ @observable private currentX: number = 0;
+ @observable private visibleStart: number = 0;
+ private readonly DEFAULT_HEIGHT = 50;
+ private readonly DEFAULT_WIDTH = 300;
+
+ componentDidMount = () => {
+ this.setOverviewWidth();
+
+ this._authoringReaction = reaction(
+ () => this.props.parent._isAuthoring,
+ () => {
+ if (!this.props.parent._isAuthoring) {
+ runInAction(() => {
+ this.setOverviewWidth();
+ });
+ }
+ },
+ );
+ }
+
+ componentWillUnmount = () => {
+ this._authoringReaction && this._authoringReaction();
+ }
+
+ @action
+ setOverviewWidth() {
+ const width1 = this.authoringContainer.current?.clientWidth;
+ const width2 = this.playbackContainer.current?.clientWidth;
+ if (width1 && width1 !== 0) this.overviewBarWidth = width1;
+ if (width2 && width2 !== 0) this.playbarWidth = width2;
+
+ if (this.props.isAuthoring) {
+ this.activeOverviewWidth = this.overviewBarWidth;
+ }
+ else {
+ this.activeOverviewWidth = this.playbarWidth;
+ }
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPanX);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPanX = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const movX = (this.props.visibleStart / this.props.totalLength) * (this.DEFAULT_WIDTH) + e.movementX;
+ this.props.movePanX((movX / (this.DEFAULT_WIDTH)) * this.props.totalLength);
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", this.onPanX);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onScrubberDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ document.addEventListener("pointermove", this.onScrubberMove);
+ document.addEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ onScrubberMove = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const scrubberRef = this._scrubberRef.current!;
+ const left = scrubberRef.getBoundingClientRect().left;
+ const offsetX = Math.round(e.clientX - left);
+ this.props.changeCurrentBarX((((offsetX) / this.activeOverviewWidth) * this.props.totalLength) + this.props.currentBarX);
+ }
+
+ @action
+ onScrubberUp = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onScrubberMove);
+ document.removeEventListener("pointerup", this.onScrubberUp);
+ }
+
+ @action
+ getTimes() {
+ const vis = KeyframeFunc.convertPixelTime(this.props.visibleLength, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const x = KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const start = KeyframeFunc.convertPixelTime(this.props.visibleStart, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ this.visibleTime = vis;
+ this.currentX = x;
+ this.visibleStart = start;
+ }
+
+ render() {
+ this.setOverviewWidth();
+ this.getTimes();
+
+ const percentVisible = this.visibleTime / this.props.time;
+ const visibleBarWidth = percentVisible * this.activeOverviewWidth;
+
+ const percentScrubberStart = this.currentX / this.props.time;
+ let scrubberStart = this.props.currentBarX / this.props.totalLength * this.activeOverviewWidth;
+ if (scrubberStart > this.activeOverviewWidth) scrubberStart = this.activeOverviewWidth;
+
+ const percentBarStart = this.visibleStart / this.props.time;
+ const barStart = percentBarStart * this.activeOverviewWidth;
+
+ let playWidth = (this.props.currentBarX / this.props.totalLength) * this.activeOverviewWidth;
+ if (playWidth > this.activeOverviewWidth) playWidth = this.activeOverviewWidth;
+
+ const timeline = this.props.isAuthoring ? [
+
+ <div key="timeline-overview-container" className="timeline-overview-container overviewBar" id="timelineOverview" ref={this.authoringContainer}>
+ <div ref={this._visibleRef} key="1" className="timeline-overview-visible" style={{ left: `${barStart}px`, width: `${visibleBarWidth}px` }} onPointerDown={this.onPointerDown}></div>,
+ <div ref={this._scrubberRef} key="2" className="timeline-overview-scrubber-container" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}>
+ <div key="timeline-overview-scrubber-head" className="timeline-overview-scrubber-head"></div>
+ </div>
+ </div>
+ ] : [
+ <div key="1" className="timeline-play-bar overviewBar" id="timelinePlay" ref={this.playbackContainer}>
+ <div ref={this._scrubberRef} className="timeline-play-head" style={{ left: `${scrubberStart}px` }} onPointerDown={this.onScrubberDown}></div>
+ </div>,
+ <div key="2" className="timeline-play-tail" style={{ width: `${playWidth}px` }}></div>
+ ];
+ return (
+ <div className="timeline-flex">
+ <div className="timelineOverview-bounding">
+ {timeline}
+ </div>
+ </div>
+ );
+ }
+
+}
+
+
diff --git a/src/client/views/animationtimeline/Track.scss b/src/client/views/animationtimeline/Track.scss
new file mode 100644
index 000000000..aec587a79
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.scss
@@ -0,0 +1,15 @@
+@import "./../globalCssVariables.scss";
+
+.track-container {
+
+ .track {
+ .inner {
+ top: 0px;
+ width: calc(100%);
+ background-color: $light-color;
+ border: 1px solid $dark-color;
+ position: relative;
+ z-index: 100;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
new file mode 100644
index 000000000..461db4858
--- /dev/null
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -0,0 +1,380 @@
+import { action, computed, intercept, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { Copy } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { Transform } from "../../util/Transform";
+import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe";
+import "./Track.scss";
+
+interface IProps {
+ node: Doc;
+ currentBarX: number;
+ transform: Transform;
+ collection: Doc;
+ time: number;
+ tickIncrement: number;
+ tickSpacing: number;
+ timelineVisible: boolean;
+ changeCurrentBarX: (x: number) => void;
+}
+
+@observer
+export class Track extends React.Component<IProps> {
+ @observable private _inner = React.createRef<HTMLDivElement>();
+ @observable private _currentBarXReaction: any;
+ @observable private _timelineVisibleReaction: any;
+ @observable private _autoKfReaction: any;
+ @observable private _newKeyframe: boolean = false;
+ private readonly MAX_TITLE_HEIGHT = 75;
+ private _trackHeight = 0;
+ private primitiveWhitelist = [
+ "x",
+ "y",
+ "_width",
+ "_height",
+ "opacity",
+ ];
+ private objectWhitelist = [
+ "data"
+ ];
+
+ @computed private get regions() { return DocListCast(this.props.node.regions); }
+ @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); }
+
+ async componentDidMount() {
+ const regions = await DocListCastAsync(this.props.node.regions);
+ if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
+ //these two lines are exactly same from timeline.tsx
+ const relativeHeight = window.innerHeight / 20;
+ this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT; //for responsiveness
+ this._timelineVisibleReaction = this.timelineVisibleReaction();
+ this._currentBarXReaction = this.currentBarXReaction();
+ if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
+ this.props.node.hidden = false;
+ this.props.node.opacity = 1;
+ // this.autoCreateKeyframe();
+ }
+
+ /**
+ * mainly for disposing reactions
+ */
+ componentWillUnmount() {
+ this._currentBarXReaction?.();
+ this._timelineVisibleReaction?.();
+ this._autoKfReaction?.();
+ }
+ ////////////////////////////////
+
+
+ getLastRegionTime = () => {
+ let lastTime: number = 0;
+ let lastRegion: Opt<Doc>;
+ this.regions.forEach(region => {
+ const time = NumCast(region.position);
+ if (lastTime <= time) {
+ lastTime = time;
+ lastRegion = region;
+ }
+ });
+ return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0;
+ }
+
+ /**
+ * keyframe save logic. Needs to be changed so it's more efficient
+ *
+ */
+ @action
+ saveKeyframe = async () => {
+ const keyframes = Cast(this.saveStateRegion?.keyframes, listSpec(Doc)) as List<Doc>;
+ const kfIndex = keyframes.indexOf(this.saveStateKf!);
+ const kf = keyframes[kfIndex] as Doc; //index in the keyframe
+ if (this._newKeyframe) {
+ DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => {
+ this.copyDocDataToKeyFrame(kf);
+ kf.opacity = (index === 0 || index === 3) ? 0.1 : 1;
+ });
+ this._newKeyframe = false;
+ }
+ if (!kf) return;
+ if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades
+ this.copyDocDataToKeyFrame(kf);
+ const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists
+ const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists
+ if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades
+ const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ leftkf && this.copyDocDataToKeyFrame(leftkf);
+ edge && (edge.opacity = 0.1);
+ leftkf && (leftkf.opacity = 1);
+ }
+ if (rightkf?.type === KeyframeFunc.KeyframeType.fade) {
+ const edge = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, rightkf);
+ edge && this.copyDocDataToKeyFrame(edge);
+ rightkf && this.copyDocDataToKeyFrame(rightkf);
+ edge && (edge.opacity = 0.1);
+ rightkf && (rightkf.opacity = 1);
+ }
+ }
+ keyframes[kfIndex] = kf;
+ this.saveStateKf = undefined;
+ this.saveStateRegion = undefined;
+ }
+
+
+ /**
+ * autocreates keyframe
+ */
+ @action
+ autoCreateKeyframe = () => {
+ const objects = this.objectWhitelist.map(key => this.props.node[key]);
+ intercept(this.props.node, change => {
+ console.log(change);
+ return change;
+ });
+ return reaction(() => {
+ return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ }, (changed, reaction) => {
+ //check for region
+ const region = this.findRegion(this.time);
+ if (region !== undefined) { //if region at scrub time exist
+ const r = region as RegionData; //for some region is returning undefined... which is not the case
+ if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot
+ this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default);
+ }
+ }
+ }, { fireImmediately: false });
+ }
+
+
+
+ // @observable private _storedState:(Doc | undefined) = undefined;
+ // /**
+ // * reverting back to previous state before editing on AT
+ // */
+ // @action
+ // revertState = () => {
+ // if (this._storedState) this.applyKeys(this._storedState);
+ // }
+
+
+ /**
+ * Reaction when scrubber bar changes
+ * made into function so it's easier to dispose later
+ */
+ @action
+ currentBarXReaction = () => {
+ return reaction(() => this.props.currentBarX, () => {
+ const regiondata = this.findRegion(this.time);
+ if (regiondata) {
+ this.props.node.hidden = false;
+ // if (!this._autoKfReaction) {
+ // // console.log("creating another reaction");
+ // // this._autoKfReaction = this.autoCreateKeyframe();
+ // }
+ this.timeChange();
+ } else {
+ this.props.node.hidden = true;
+ this.props.node.opacity = 0;
+ //if (this._autoKfReaction) this._autoKfReaction();
+ }
+ });
+ }
+
+ /**
+ * when timeline is visible, reaction is ran so states are reverted
+ */
+ @action
+ timelineVisibleReaction = () => {
+ return reaction(() => {
+ return this.props.timelineVisible;
+ }, isVisible => {
+ if (isVisible) {
+ this.regions.filter(region => !region.hasData).forEach(region => {
+ for (let i = 0; i < 4; i++) {
+ this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
+ if (i === 0 || i === 3) { //manually inputing fades
+ DocListCast(region.keyframes)[i].opacity = 0.1;
+ }
+ }
+ });
+ } else {
+ console.log("reverting state");
+ //this.revertState();
+ }
+ });
+ }
+
+ @observable private saveStateKf: (Doc | undefined) = undefined;
+ @observable private saveStateRegion: (Doc | undefined) = undefined;
+
+ /**w
+ * when scrubber position changes. Need to edit the logic
+ */
+ @action
+ timeChange = async () => {
+ if (this.saveStateKf !== undefined) {
+ await this.saveKeyframe();
+ } else if (this._newKeyframe) {
+ await this.saveKeyframe();
+ }
+ const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on
+ if (regiondata) {
+ const leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
+ const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
+ const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
+ if (currentkf) {
+ console.log("is current");
+ await this.applyKeys(currentkf);
+ this.saveStateKf = currentkf;
+ this.saveStateRegion = regiondata;
+ } else if (leftkf && rightkf) {
+ await this.interpolate(leftkf, rightkf);
+ }
+ }
+ }
+
+ /**
+ * applying changes (when saving the keyframe)
+ * need to change the logic here
+ */
+ @action
+ private applyKeys = async (kf: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (!kf[key]) {
+ this.props.node[key] = undefined;
+ } else {
+ const stored = kf[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+
+ /**
+ * calculating current keyframe, if the scrubber is right on the keyframe
+ */
+ @action
+ calcCurrent = (region: Doc) => {
+ let currentkf: (Doc | undefined) = undefined;
+ const keyframes = DocListCast(region.keyframes!);
+ keyframes.forEach((kf) => {
+ if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf;
+ });
+ return currentkf;
+ }
+
+
+ /**
+ * basic linear interpolation function
+ */
+ @action
+ interpolate = async (left: Doc, right: Doc) => {
+ this.primitiveWhitelist.forEach(key => {
+ if (left[key] && right[key] && typeof (left[key]) === "number" && typeof (right[key]) === "number") { //if it is number, interpolate
+ const dif = NumCast(right[key]) - NumCast(left[key]);
+ const deltaLeft = this.time - NumCast(left.time);
+ const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
+ this.props.node[key] = NumCast(left[key]) + (dif * ratio);
+ } else { // case data
+ const stored = left[key];
+ this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ }
+ });
+ }
+
+ /**
+ * finds region that corresponds to specific time (is there a region at this time?)
+ * linear O(n) (maybe possible to optimize this with other Data structures?)
+ */
+ findRegion = (time: number) => {
+ return this.regions?.find(rd => (time >= NumCast(rd.position) && time <= (NumCast(rd.position) + NumCast(rd.duration))));
+ }
+
+
+ /**
+ * double click on track. Signalling keyframe creation.
+ */
+ @action
+ onInnerDoubleClick = (e: React.MouseEvent) => {
+ const inner = this._inner.current!;
+ const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale);
+ this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
+ }
+
+
+ /**
+ * creates a region (KEYFRAME.TSX stuff).
+ */
+ @action
+ createRegion = (time: number) => {
+ if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions)
+ const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data
+
+ regiondata.position = time; //set position
+ const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);
+
+ if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region
+ regiondata.duration = rightRegion.position - regiondata.position;
+ }
+ if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
+ Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata);
+ this._newKeyframe = true;
+ this.saveStateRegion = regiondata;
+ return regiondata;
+ }
+ }
+ }
+
+ @action
+ makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time
+ const trackKeyFrames = DocListCast(regiondata.keyframes);
+ const existingkf = trackKeyFrames.find(TK => TK.time === time);
+ if (existingkf) return existingkf;
+ //else creates a new doc.
+ const newKeyFrame: Doc = new Doc();
+ newKeyFrame.time = time;
+ newKeyFrame.type = type;
+ this.copyDocDataToKeyFrame(newKeyFrame);
+ //assuming there are already keyframes (for keeping keyframes in order, sorted by time)
+ if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame);
+ trackKeyFrames.map(kf => NumCast(kf.time)).forEach((kfTime, index) => {
+ if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
+ regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
+ }
+ });
+ return newKeyFrame;
+ }
+
+ @action
+ copyDocDataToKeyFrame = (doc: Doc) => {
+ this.primitiveWhitelist.map(key => {
+ const originalVal = this.props.node[key];
+ doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
+ });
+ }
+
+ /**
+ * UI sstuff here. Not really much to change
+ */
+ render() {
+ return (
+ <div className="track-container">
+ <div className="track">
+ <div className="inner" ref={this._inner} style={{ height: `${this._trackHeight}px` }}
+ onDoubleClick={this.onInnerDoubleClick}
+ onPointerOver={() => Doc.BrushDoc(this.props.node)}
+ onPointerOut={() => Doc.UnBrushDoc(this.props.node)} >
+ {this.regions?.map((region, i) => {
+ return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
+ })}
+ </div>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2453acddf..a04136e51 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,21 +2,22 @@ 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 { documentSchema, collectionSchema } from '../../../new_fields/documentSchemas';
import { makeInterface } from '../../../new_fields/Schema';
-import { NumCast, StrCast } from '../../../new_fields/Types';
+import { NumCast, StrCast, ScriptCast, Cast } 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';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { ContextMenu } from '../ContextMenu';
import { ObjectField } from '../../../new_fields/ObjectField';
+import { returnFalse } from '../../../Utils';
-type CarouselDocument = makeInterface<[typeof documentSchema,]>;
-const CarouselDocument = makeInterface(documentSchema);
+type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
+const CarouselDocument = makeInterface(documentSchema, collectionSchema);
@observer
export class CollectionCarouselView extends CollectionSubView(CarouselDocument) {
@@ -39,7 +40,6 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
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);
@@ -47,13 +47,29 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
<>
<div className="collectionCarouselView-image" key="image">
<ContentFittingDocumentView {...this.props}
+ onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)}
+ onClick={ScriptCast(this.layoutDoc.onChildClick)}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
Document={this.childLayoutPairs[index].layout}
- DataDocument={this.childLayoutPairs[index].data}
+ DataDoc={this.childLayoutPairs[index].data}
PanelHeight={this.panelHeight}
- getTransform={this.props.ScreenToLocalTransform} />
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ bringToFront={returnFalse}
+ parentActive={this.props.active}
+ />
</div>
- <div className="collectionCarouselView-caption" key="caption" style={{ background: this.props.backgroundColor?.(this.props.Document) }}>
- <FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox>
+ <div className="collectionCarouselView-caption" key="caption"
+ style={{
+ background: StrCast(this.layoutDoc._captionBackgroundColor, this.props.backgroundColor?.(this.props.Document)),
+ color: StrCast(this.layoutDoc._captionColor, StrCast(this.layoutDoc.color)),
+ borderRadius: StrCast(this.layoutDoc._captionBorderRounding),
+ }}>
+ <FormattedTextBox key={index} {...this.props}
+ xMargin={NumCast(this.layoutDoc["_carousel-caption-xMargin"])}
+ yMargin={NumCast(this.layoutDoc["_carousel-caption-yMargin"])}
+ Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"} />
</div>
</>;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 0d859c3f1..33ece13cc 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -68,10 +68,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
+ DragManager.Vals.Instance.StartWindowDrag = this.StartOtherDrag;
}
hack: boolean = false;
undohack: any = null;
- public StartOtherDrag(e: any, dragDocs: Doc[]) {
+ public StartOtherDrag = (e: any, dragDocs: Doc[]) => {
let config: any;
if (dragDocs.length === 1) {
config = CollectionDockingView.makeDocumentConfig(dragDocs[0]);
@@ -499,7 +500,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
const stack = tab.contentItem.parent;
// shifts the focus to this tab when another tab is dragged over it
tab.element[0].onmouseenter = (e: any) => {
- if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return;
+ if (!this._isPointerDown || !DragManager.Vals.Instance.GetIsDragging()) return;
const activeContentItem = tab.header.parent.getActiveContentItem();
if (tab.contentItem !== activeContentItem) {
tab.header.parent.setActiveContentItem(tab.contentItem);
@@ -685,6 +686,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
if (curPres) {
const pinDoc = Doc.MakeAlias(doc);
pinDoc.presentationTargetDoc = doc;
+ pinDoc.presZoomButton = true;
Doc.AddDocToList(curPres, "data", pinDoc);
if (!DocumentManager.Instance.getDocumentView(curPres)) {
CollectionDockingView.AddRightSplit(curPres);
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index 7b7828d7d..971224482 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -1,4 +1,4 @@
-import { GoogleApiWrapper, Map as GeoMap, MapProps, Marker } from "google-maps-react";
+import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react";
import { observer } from "mobx-react";
import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
@@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => {
};
@observer
-class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> & { google: any }>(MapSchema) {
+class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) {
private _cancelAddrReq = new Map<string, boolean>();
private _cancelLocReq = new Map<string, boolean>();
@@ -221,7 +221,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
zoom={center.zoom || 10}
initialCenter={center}
center={center}
- onIdle={(_props?: MapProps, map?: google.maps.Map) => {
+ onIdle={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset zoom (ideally, we could probably can tell the map to disallow zooming somehow instead)
map?.setZoom(center?.zoom || 10);
@@ -233,7 +233,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<MapProps> &
}))();
}
}}
- onDragend={(_props?: MapProps, map?: google.maps.Map) => {
+ onDragend={(_props?: IMapProps, map?: google.maps.Map) => {
if (this.layoutDoc.lockedTransform) {
// reset the drag (ideally, we could probably can tell the map to disallow dragging somehow instead)
map?.setCenter({ lat: center?.lat!, lng: center?.lng! });
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 42f0c4311..c74cfbcf4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -2,13 +2,13 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
import { StrCast, NumCast } from "../../../new_fields/Types";
-import { numberRange } from "../../../Utils";
+import { numberRange, setupMoveUpEvents, emptyFunction } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
@@ -44,37 +44,44 @@ interface CMVFieldRowProps {
export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> {
@observable private _background = "inherit";
@observable private _createAliasSelected: boolean = false;
- @observable private _collapsed: boolean = false;
- @observable private _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
- @observable private _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ @observable private heading: string = "";
+ @observable private color: string = "#f1efeb";
+ @observable private collapsed: boolean = false;
+ private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); }
+ private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); }
+ private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); }
private _dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
private _contRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _sensitivity: number = 16;
private _ele: any;
createRowDropRef = (ele: HTMLDivElement | null) => {
- this._dropDisposer && this._dropDisposer();
+ this._dropDisposer?.();
if (ele) {
this._ele = ele;
this.props.observeHeight(ele);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
}
}
+ @action
+ componentDidMount() {
+ this.heading = this.props.headingObject?.heading || "";
+ this.color = this.props.headingObject?.color || "#f1efeb";
+ this.collapsed = this.props.headingObject?.collapsed || false;
+ }
componentWillUnmount() {
this.props.unobserveHeight(this._ele);
}
getTrueHeight = () => {
- if (this._collapsed) {
- this.props.setDocHeight(this._heading, 20);
+ if (this.collapsed) {
+ this.props.setDocHeight(this.heading, 20);
} else {
const rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
const transformScale = this.props.screenToLocalTransform().Scale;
const trueHeight = rawHeight * transformScale;
- this.props.setDocHeight(this._heading, trueHeight);
+ this.props.setDocHeight(this.heading, trueHeight);
}
}
@@ -86,7 +93,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
(this.props.parent.Document.dropConverter instanceof ScriptField) &&
this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData });
const key = StrCast(this.props.parent.props.Document._pivotField);
- const castedValue = this.getValue(this._heading);
+ const castedValue = this.getValue(this.heading);
de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue);
this.props.parent.onInternalDrop(e, de);
e.stopPropagation();
@@ -113,10 +120,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
}
this.props.docList.forEach(d => d[key] = castedValue);
- if (this.props.headingObject) {
- this.props.headingObject.setHeading(castedValue.toString());
- this._heading = this.props.headingObject.heading;
- }
+ this._heading = castedValue.toString();
return true;
}
return false;
@@ -125,19 +129,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
changeColumnColor = (color: string) => {
this._createAliasSelected = false;
- if (this.props.headingObject) {
- this.props.headingObject.setColor(color);
- this._color = color;
- }
+ this._color = color;
}
- pointerEnteredRow = action(() => SelectionManager.GetIsDragging() && (this._background = "#b4b4b4"));
+ pointerEnteredRow = action(() => DragManager.Vals.Instance.GetIsDragging() && (this._background = "#b4b4b4"));
@action
pointerLeaveRow = () => {
this._createAliasSelected = false;
this._background = "inherit";
- document.removeEventListener("pointermove", this.startDrag);
}
@action
@@ -161,65 +161,36 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}));
@action
- collapseSection = () => {
+ collapseSection = (e: any) => {
this._createAliasSelected = false;
- if (this.props.headingObject) {
- this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
- this.toggleVisibility();
- }
- }
-
- startDrag = (e: PointerEvent) => {
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
- if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
- const alias = Doc.MakeAlias(this.props.parent.props.Document);
- const key = StrCast(this.props.parent.props.Document._pivotField);
- let value = this.getValue(this._heading);
- value = typeof value === "string" ? `"${value}"` : value;
- const script = `return doc.${key} === ${value}`;
- const compiled = CompileScript(script, { params: { doc: Doc.name } });
- if (compiled.compiled) {
- alias.viewSpecScript = new ScriptField(compiled);
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
- }
-
- e.stopPropagation();
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- }
- }
-
- pointerUp = (e: PointerEvent) => {
+ this.toggleVisibility();
e.stopPropagation();
- e.preventDefault();
+ }
- if (this.props.parent.props.Document._chromeStatus === "disabled") {
- this.collapseSection();
+ headerMove = (e: PointerEvent) => {
+ const alias = Doc.MakeAlias(this.props.parent.props.Document);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
+ let value = this.getValue(this.heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ const script = `return doc.${key} === ${value}`;
+ const compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ alias.viewSpecScript = new ScriptField(compiled);
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
}
-
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
+ return true;
}
@action
headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
-
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
- this._startDragPosition = { x: dx, y: dy };
-
- if (this.props.parent.props.Document._chromeStatus === "disabled" || this._createAliasSelected) {
- document.removeEventListener("pointermove", this.startDrag);
- document.addEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- document.addEventListener("pointerup", this.pointerUp);
+ if (e.button === 0 && !e.ctrlKey) {
+ setupMoveUpEvents(this, e, this.headerMove, emptyFunction, () => (this.props.parent.props.Document._chromeStatus === "disabled") && this.collapseSection(e));
+ this._createAliasSelected = false;
}
- this._createAliasSelected = false;
}
renderColorPicker = () => {
- const selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ const selected = this.color;
const pink = PastelSchemaPalette.get("pink2");
const purple = PastelSchemaPalette.get("purple4");
@@ -249,7 +220,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
toggleAlias = action(() => this._createAliasSelected = true);
- toggleVisibility = action(() => this._collapsed = !this._collapsed);
+ toggleVisibility = () => this._collapsed = !this.collapsed;
renderMenu = () => {
const selected = this._createAliasSelected;
@@ -264,7 +235,6 @@ 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 newEditableViewProps = {
GetValue: () => "",
@@ -272,9 +242,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
contents: "+ NEW",
HeadingObject: this.props.headingObject,
toggle: this.toggleVisibility,
- color: this._color
+ color: this.color
};
- return collapsed ? (null) :
+ return this.collapsed ? (null) :
<div style={{ position: "relative" }}>
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
<div className="collectionStackingView-addDocumentButton"
@@ -300,10 +270,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
}
@computed get headingView() {
- const heading = this._heading;
const noChrome = this.props.parent.props.Document._chromeStatus === "disabled";
const key = StrCast(this.props.parent.props.Document._pivotField);
- const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
const headerEditableViewProps = {
GetValue: () => evContents,
SetValue: this.headingChanged,
@@ -311,7 +280,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
oneLine: true,
HeadingObject: this.props.headingObject,
toggle: this.toggleVisibility,
- color: this._color
+ color: this.color
};
return this.props.parent.props.Document.miniHeaders ?
<div className="collectionStackingView-miniHeader">
@@ -319,11 +288,11 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div> :
!this.props.headingObject ? (null) :
<div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
- <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} onClick={noChrome ? undefined : this.collapseSection}
+ <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
title={evContents === `NO ${key.toUpperCase()} VALUE` ?
`Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
- style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey" }}>
- <EditableView {...headerEditableViewProps} />
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this.color : "lightgrey" }}>
+ {noChrome ? evContents : <EditableView {...headerEditableViewProps} />}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
<Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
@@ -334,7 +303,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div>
}
{noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}>
- <FontAwesomeIcon icon={this._collapsed ? "chevron-down" : "chevron-up"} size="lg" />
+ <FontAwesomeIcon icon={this.collapsed ? "chevron-down" : "chevron-up"} size="lg" />
</button>}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionOptions">
@@ -349,17 +318,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
</div>;
}
render() {
- const background = this._background; //to account for observables in Measure
- const contentlayout = this.contentLayout;
- const headingview = this.headingView;
+ const background = this._background;
return <div className="collectionStackingView-masonrySection"
style={{ width: this.props.parent.NodeWidth, background }}
ref={this.createRowDropRef}
onPointerEnter={this.pointerEnteredRow}
onPointerLeave={this.pointerLeaveRow}
>
- {headingview}
- {contentlayout}
+ {this.headingView}
+ {this.contentLayout}
</div >;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 3bbfcc4d7..0a10c24b3 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -12,6 +12,7 @@ import React = require("react");
import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../../Utils";
import { SelectionManager } from "../../util/SelectionManager";
import { UndoManager } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
@observer
export class CollectionPileView extends CollectionSubView(doc => doc) {
@@ -53,7 +54,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
toggleStarburst = action(() => {
if (this._layoutEngine === 'starburst') {
const defaultSize = 110;
- this.layoutDoc.overflow = undefined;
+ this.layoutDoc._overflow = undefined;
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
@@ -61,7 +62,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
this._layoutEngine = 'pass';
} else {
const defaultSize = 25;
- this.layoutDoc.overflow = 'visible';
+ this.layoutDoc._overflow = 'visible';
!this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 500);
!this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
if (this._layoutEngine === 'pass') {
@@ -78,7 +79,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
_undoBatch: UndoManager.Batch | undefined;
pointerDown = (e: React.PointerEvent) => {
let dist = 0;
- SelectionManager.SetIsDragging(true);
+ DragManager.Vals.Instance.SetIsDragging(true);
// this._lastTap should be set to 0, and this._doubleTap should be set to false in the class header
setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
if (this.layoutEngine() === "pass" && this.childDocs.length && this.props.isSelected(true)) {
@@ -98,7 +99,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
}, () => {
this._undoBatch?.end();
this._undoBatch = undefined;
- SelectionManager.SetIsDragging(false);
+ DragManager.Vals.Instance.SetIsDragging(false);
if (!this.childDocs.length) {
this.props.ContainingCollectionView?.removeDocument(this.props.Document);
}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 82204ca7b..0e6489947 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -189,7 +189,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
};
const onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging() && (type === "document" || type === undefined)) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging() && (type === "document" || type === undefined)) {
dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
}
};
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 972714e34..f9cd9a628 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -32,7 +32,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
@@ -143,7 +143,7 @@ export class MovableRow extends React.Component<MovableRowProps> {
private _rowDropDisposer?: DragManager.DragDropDisposer;
onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-row-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 380d91d2f..c0024293f 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -27,7 +27,7 @@ import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionView } from "./CollectionView";
import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
-import { setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils";
+import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils";
import { DocumentView } from "../nodes/DocumentView";
library.add(faCog, faPlus, faSortUp, faSortDown);
@@ -121,7 +121,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
{!this.previewDocument ? (null) :
<ContentFittingDocumentView
Document={this.previewDocument}
- DataDocument={undefined}
+ DataDoc={undefined}
NativeHeight={returnZero}
NativeWidth={returnZero}
fitToBox={true}
@@ -132,16 +132,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
rootSelected={this.rootSelected}
PanelWidth={this.previewWidth}
PanelHeight={this.previewHeight}
- getTransform={this.getPreviewTransform}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ScreenToLocalTransform={this.getPreviewTransform}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
moveDocument={this.props.moveDocument}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>}
</div>;
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 47faa9239..5eaf29316 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -180,13 +180,16 @@
.collectionStackingView-sectionHeader-subCont {
outline: none;
border: 0px;
- color: $light-color;
width: 100%;
- color: grey;
letter-spacing: 2px;
font-size: 75%;
transition: transform 0.2s;
position: relative;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: lightGray;
.editableView-container-editing-oneLine,
.editableView-container-editing {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e3720bf01..f6cdebc9b 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -4,15 +4,17 @@ 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, DataSym } from "../../../new_fields/Doc";
+import { DataSym, Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
+import { listSpec, makeInterface } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types";
import { TraceMobx } from "../../../new_fields/util";
-import { Utils, setupMoveUpEvents, emptyFunction, returnZero, returnOne } from "../../../Utils";
+import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from "../../../Utils";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
import { ContextMenu } from "../ContextMenu";
@@ -24,11 +26,14 @@ import "./CollectionStackingView.scss";
import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionViewType } from "./CollectionView";
-import { SelectionManager } from "../../util/SelectionManager";
+import { ScriptField } from "../../../new_fields/ScriptField";
const _global = (window /* browser */ || global /* node */) as any;
+type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>;
+const StackingDocument = makeInterface(collectionSchema, documentSchema);
+
@observer
-export class CollectionStackingView extends CollectionSubView(doc => doc) {
+export class CollectionStackingView extends CollectionSubView(StackingDocument) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_pivotFieldDisposer?: IReactionDisposer;
@@ -116,7 +121,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
getSimpleDocHeight(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
@@ -128,7 +133,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return layoutDoc._fitWidth ? wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1) : layoutDoc[HeightSym]();
}
componentDidMount() {
- super.componentDidMount();
+ super.componentDidMount?.();
// reset section headers when a new filter is inputted
this._pivotFieldDisposer = reaction(
@@ -150,8 +155,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
}
- @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
- @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
+ @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
@@ -160,14 +165,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
return this.props.addDocTab(doc, where);
}
+
getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
- const layoutDoc = Doc.Layout(doc, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(doc, this.props.ChildLayoutTemplate?.());
const height = () => this.getDocHeight(doc);
return <ContentFittingDocumentView
Document={doc}
- DataDocument={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
+ DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -175,33 +182,36 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
- onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
- getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
pinToPres={this.props.pinToPres}
/>;
}
getDocWidth(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
}
getDocHeight(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
@@ -302,7 +312,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.refList.push(ref);
const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
this.observer = new _global.ResizeObserver(action((entries: any) => {
- if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) {
+ if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) {
Doc.Layout(doc)._height = Math.min(1200, Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))));
}
}));
@@ -349,7 +359,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
this.refList.push(ref);
const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
this.observer = new _global.ResizeObserver(action((entries: any) => {
- if (this.props.Document._autoHeight && ref && this.refList.length && !SelectionManager.GetIsDragging()) {
+ if (this.props.Document._autoHeight && ref && this.refList.length && !DragManager.Vals.Instance.GetIsDragging()) {
Doc.Layout(doc)._height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0);
}
}));
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 323d7be25..1d16a5478 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -120,7 +120,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
pointerEntered = () => {
- if (SelectionManager.GetIsDragging()) {
+ if (DragManager.Vals.Instance.GetIsDragging()) {
this._background = "#b4b4b4";
}
}
@@ -322,11 +322,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
title={evContents === `NO ${key.toUpperCase()} VALUE` ?
`Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
- style={{
- width: "100%",
- background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit",
- color: "grey"
- }}>
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}>
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
@@ -359,7 +355,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<div className="collectionStackingViewFieldColumn" key={heading}
style={{
width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`,
- height: undefined, // SelectionManager.GetIsDragging() ? "100%" : undefined,
+ height: undefined, // DragManager.Vals.Instance.GetIsDragging() ? "100%" : undefined,
background: this._background
}}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 49abc6ee6..b4ca29b19 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,30 +1,30 @@
import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { basename } from 'path';
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
+import { GestureUtils } from "../../../pen-gestures/GestureUtils";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Upload } from "../../../server/SharedMediaTypes";
import { Utils } from "../../../Utils";
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { DocServer } from "../../DocServer";
-import { DocumentType } from "../../documents/DocumentTypes";
import { Docs, DocumentOptions } from "../../documents/Documents";
-import { DragManager, dropActionType } from "../../util/DragManager";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Networking } from "../../Network";
+import { DragManager } from "../../util/DragManager";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { InteractionUtils } from "../../util/InteractionUtils";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
import { CollectionView } from "./CollectionView";
import React = require("react");
-import { basename } from 'path';
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { ImageUtils } from "../../util/Import & Export/ImageUtils";
-import { Networking } from "../../Network";
-import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { InteractionUtils } from "../../util/InteractionUtils";
-import { Upload } from "../../../server/SharedMediaTypes";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
@@ -43,6 +43,10 @@ export interface CollectionViewProps extends FieldViewProps {
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
children?: never | (() => JSX.Element[]) | React.ReactNode;
+ ChildLayoutTemplate?: () => Doc;
+ ChildLayoutString?: string;
+ childClickScript?: ScriptField;
+ childDoubleClickScript?: ScriptField;
freezeChildDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height
overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
ignoreFields?: string[]; // used in TreeView to ignore specified fields (see LinkBox)
@@ -56,7 +60,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
private dropDisposer?: DragManager.DragDropDisposer;
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- private _childLayoutDisposer?: IReactionDisposer;
protected _mainCont?: HTMLDivElement;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
@@ -64,7 +67,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.multiTouchDisposer?.();
if (ele) {
this._mainCont = ele;
- this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
+ this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
}
@@ -73,25 +76,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
- componentDidMount() {
- this._childLayoutDisposer = reaction(() => ({ childDocs: this.childDocs, childLayout: Cast(this.props.Document.childLayout, Doc) }),
- ({ childDocs, childLayout }) => {
- if (childLayout instanceof Doc) {
- childDocs.map(doc => {
- doc.layout_fromParent = childLayout;
- doc.layoutKey = "layout_fromParent";
- });
- }
- else if (!(childLayout instanceof Promise)) {
- childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout"));
- }
- }, { fireImmediately: true });
-
- }
componentWillUnmount() {
this.gestureDisposer?.();
this.multiTouchDisposer?.();
- this._childLayoutDisposer?.();
}
@computed get dataDoc() {
@@ -208,6 +195,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
}
+ protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) {
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.draggedDocuments.some(d => this.childDocs.includes(d))) {
+ de.complete.docDragData.dropAction = "move";
+ }
+ e.stopPropagation();
+ }
+ }
+
@undoBatch
@action
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
@@ -332,23 +328,23 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
}));
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 proto = newBox.proto!;
- const documentId = matches[2];
- proto[GoogleRef] = documentId;
- proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
- proto.backgroundColor = "#eeeeff";
- addDocument(newBox);
- return;
- }
- if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
- const albumId = matches[3];
- const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
- console.log(mediaItems);
- 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 proto = newBox.proto!;
+ // const documentId = matches[2];
+ // proto[GoogleRef] = documentId;
+ // proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
+ // proto.backgroundColor = "#eeeeff";
+ // addDocument(newBox);
+ // return;
+ // }
+ // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
+ // const albumId = matches[3];
+ // const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
+ // console.log(mediaItems);
+ // return;
+ // }
}
const { items } = e.dataTransfer;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 045134225..a2d4774c8 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -28,7 +28,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@observable _childClickedScript: Opt<ScriptField>;
@observable _viewDefDivClick: Opt<ScriptField>;
async componentDidMount() {
- const detailView = (await DocCastAsync(this.props.Document.childDetailView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), "");
+ const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), "");
const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
runInAction(() => {
this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! });
@@ -84,7 +84,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
@computed get contents() {
return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%", pointerEvents: this.props.active() ? undefined : "none" }} onPointerDown={this.contentsDown}>
- <CollectionFreeFormView {...this.props} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} fitToBox={true} freezeChildDimensions={BoolCast(this.layoutDoc._freezeChildDimensions, true)} layoutEngine={this.layoutEngine} />
+ <CollectionFreeFormView {...this.props} childClickScript={this._childClickedScript} viewDefDivClick={this._viewDefDivClick} fitToBox={true} freezeChildDimensions={true} layoutEngine={this.layoutEngine} />
</div>;
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d938bd7ad..288fa8794 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { RichTextField } from '../../../new_fields/RichTextField';
import { Document, listSpec } from '../../../new_fields/Schema';
import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types';
@@ -15,7 +14,6 @@ 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';
@@ -33,6 +31,7 @@ import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import { CollectionViewType } from './CollectionView';
import React = require("react");
+import { PrefetchProxy } from '../../../new_fields/Proxy';
export interface TreeViewProps {
@@ -148,7 +147,7 @@ class TreeView extends React.Component<TreeViewProps> {
onPointerEnter = (e: React.PointerEvent): void => {
this.props.active(true) && Doc.BrushDoc(this.dataDoc);
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
+ if (e.buttons === 1 && DragManager.Vals.Instance.GetIsDragging()) {
this._header!.current!.className = "treeViewItem-header";
document.addEventListener("pointermove", this.onDragMove, true);
}
@@ -353,27 +352,31 @@ class TreeView extends React.Component<TreeViewProps> {
return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}>
<ContentFittingDocumentView
Document={layoutDoc}
- DataDocument={this.templateDataDoc}
+ DataDoc={this.templateDataDoc}
LibraryPath={emptyPath}
renderDepth={this.props.renderDepth + 1}
rootSelected={returnTrue}
backgroundColor={this.props.backgroundColor}
fitToBox={this.boundsOfCollectionDocument !== undefined}
FreezeDimensions={true}
- NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
- NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
+ NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : returnZero}
+ NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : returnZero}
PanelWidth={panelWidth}
PanelHeight={panelHeight}
- getTransform={this.docTransform}
- CollectionDoc={this.props.containingCollection}
- CollectionView={undefined}
+ focus={returnFalse}
+ ScreenToLocalTransform={this.docTransform}
+ ContainingCollectionDoc={this.props.containingCollection}
+ ContainingCollectionView={undefined}
addDocument={returnFalse}
moveDocument={this.props.moveDocument}
removeDocument={returnFalse}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={emptyFunction}
addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres} />
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ />
</div>;
}
}
@@ -448,7 +451,7 @@ class TreeView extends React.Component<TreeViewProps> {
fontWeight: this.props.document.searchMatch ? "bold" : undefined,
textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined,
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
- pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? undefined : "none"
+ pointerEvents: this.props.active() || DragManager.Vals.Instance.GetIsDragging() ? undefined : "none"
}} >
{Doc.GetT(this.props.document, "editTitle", "boolean", true) ?
this.editableView("title") :
@@ -477,7 +480,7 @@ class TreeView extends React.Component<TreeViewProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)}
+ dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)}
ContainingCollectionView={undefined}
ContainingCollectionDoc={this.props.containingCollection}
/>}
@@ -721,14 +724,6 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
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];
- if (caption) {
- Doc.GetProto(img).caption = caption;
- }
- });
- });
const { ImageDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
@@ -738,22 +733,20 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
heroView._showTitle = "title";
heroView._showTitleHover = "titlehover";
- Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data",
- Docs.Create.FontIconDocument({
- title: "hero view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias",
- dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), icon: "portrait",
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
- }));
-
- Doc.AddDocToList(Doc.UserDoc().dockedBtns as Doc, "data",
- Docs.Create.FontIconDocument({
- title: "detail view", _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias",
- dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), icon: "file-alt",
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
- }));
-
- Document.childLayout = heroView;
- Document.childDetailView = detailView;
+ const doubleClickView = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target
+ DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
+ DocListCast(d.data).map((img, i) => {
+ const caption = (d.captions as any)[i];
+ if (caption) {
+ Doc.GetProto(img).caption = caption;
+ Doc.GetProto(img).doubleClickView = doubleClickView;
+ }
+ });
+ Doc.GetProto(d).proto = heroView; // all devices "are" heroViews that share the same layout & defaults. Seems better than making them all be independent and copy a layout string // .layout = ImageBox.LayoutString("hero");
+ });
+
+ Document.childLayoutTemplate = heroView;
+ Document.childClickedOpenTemplateView = new PrefetchProxy(detailView);
Document._viewType = CollectionViewType.Time;
Document._forceActive = true;
Document._pivotField = "company";
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e8edf7279..c22ebbcbd 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -73,6 +73,11 @@ export enum CollectionViewType {
Grid = "grid",
Pile = "pileup"
}
+export interface CollectionViewCustomProps {
+ filterAddDocument: (doc: Doc) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
+ childLayoutTemplate?: () => Opt<Doc>; // specify a layout Doc template to use for children of the collection
+ childLayoutString?: string; // specify a layout string to use for children of the collection
+}
export interface CollectionRenderProps {
addDocument: (document: Doc) => boolean;
@@ -81,10 +86,12 @@ export interface CollectionRenderProps {
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
PanelWidth: () => number;
+ ChildLayoutTemplate?: () => Doc;
+ ChildLayoutString?: string;
}
@observer
-export class CollectionView extends Touchable<FieldViewProps> {
+export class CollectionView extends Touchable<FieldViewProps & CollectionViewCustomProps> {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
private _isChildActive = false; //TODO should this be observable?
@@ -115,12 +122,16 @@ export class CollectionView extends Touchable<FieldViewProps> {
@action.bound
addDocument(doc: Doc): boolean {
+ if (this.props.filterAddDocument?.(doc) === false) {
+ return false;
+ }
+
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
!docList.includes(doc) && (targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, doc])); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there
// Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
- doc.context = this.props.Document;
targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ doc.context = this.props.Document;
Doc.GetProto(doc).lastOpened = new DateField;
return true;
}
@@ -193,8 +204,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
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" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) :
- <CollectionViewBaseChrome CollectionView={this} key="chrome" PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
- return [chrome, this.SubViewHelper(type, renderProps)];
+ <CollectionViewBaseChrome key="chrome" CollectionView={this} PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
+ return <>{chrome} {this.SubViewHelper(type, renderProps)}</>;
}
@@ -272,8 +283,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
if (this.props.Document.childLayout instanceof Doc) {
layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" });
}
- if (this.props.Document.childDetailView instanceof Doc) {
- layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailView as Doc, "onRight"), icon: "project-diagram" });
+ if (this.props.Document.childClickedOpenTemplateView instanceof Doc) {
+ layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childClickedOpenTemplateView as Doc, "onRight"), icon: "project-diagram" });
}
layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" });
@@ -281,15 +292,20 @@ export class CollectionView extends Touchable<FieldViewProps> {
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
- const funcs = [{ key: "onChildClick", name: "On Child Clicked", script: undefined as any as ScriptField }];
- DocListCast(Cast(Doc.UserDoc().childClickFuncs, Doc, null).data).forEach(childClick =>
- funcs.push({ key: "onChildClick", name: StrCast(childClick.title), script: ScriptCast(childClick.script) }));
+ const funcs = [
+ { key: "onChildClick", name: "On Child Clicked" },
+ { key: "onChildDoubleClick", name: "On Child Double Clicked" }];
funcs.map(func => onClicks.push({
description: `Edit ${func.name} script`, icon: "edit", event: (obj: any) => {
- func.script && (this.props.Document[func.key] = ObjectField.MakeCopy(func.script));
ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name });
}
}));
+ DocListCast(Cast(Doc.UserDoc()["clickFuncs-child"], Doc, null).data).forEach(childClick =>
+ onClicks.push({
+ description: `Set child ${childClick.title}`,
+ icon: "edit",
+ event: () => this.props.Document[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)),
+ }));
!existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
const more = ContextMenu.Instance.findByDescription("More...");
@@ -496,6 +512,8 @@ export class CollectionView extends Touchable<FieldViewProps> {
</div>
</div>;
}
+ childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
+ childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString);
render() {
TraceMobx();
@@ -505,7 +523,9 @@ export class CollectionView extends Touchable<FieldViewProps> {
moveDocument: this.moveDocument,
active: this.active,
whenActiveChanged: this.whenActiveChanged,
- PanelWidth: this.bodyPanelWidth
+ PanelWidth: this.bodyPanelWidth,
+ ChildLayoutTemplate: this.childLayoutTemplate,
+ ChildLayoutString: this.childLayoutString,
};
return (<div className={"collectionView"}
style={{
@@ -515,7 +535,7 @@ export class CollectionView extends Touchable<FieldViewProps> {
}}
onContextMenu={this.onContextMenu}>
{this.showIsTagged()}
- <div style={{ width: `calc(100% - ${this.facetWidth()}px)` }}>
+ <div className="collectionView-facetCont" style={{ width: `calc(100% - ${this.facetWidth()}px)` }}>
{this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
</div>
{this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d =>
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 5203eb55f..e4581eb46 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -33,6 +33,17 @@
outline-color: black;
}
+ .collectionViewBaseChrome-button{
+ font-size: 75%;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: purple;
+ outline-color: black;
+ border: none;
+ padding: 12px 10px 11px 10px;
+ margin-left: 10px;
+ }
.collectionViewBaseChrome-cmdPicker {
margin-left: 3px;
margin-right: 0px;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index d26e3a38b..62b03bbdc 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -44,9 +44,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
initialize: emptyFunction,
};
_narrativeCommand = {
- params: ["target", "source"], title: "=> click item view",
- script: "this.target.childDetailView = getDocTemplate(this.source?.[0])",
- immediate: (source: Doc[]) => this.target.childDetailView = Doc.getDocTemplate(source?.[0]),
+ params: ["target", "source"], title: "=> click clicked open view",
+ script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
+ immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]),
initialize: emptyFunction,
};
_contentCommand = {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index cf12ef382..d67d1993e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -40,7 +40,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bpt = Utils.closestPtBetweenRectangles(bbounds.left, bbounds.top, bbounds.width, bbounds.height,
abounds.left, abounds.top, abounds.width, abounds.height,
apt.point.x, apt.point.y);
- const afield = StrCast(this.props.A.props.Document[StrCast(this.props.A.props.layoutKey, "layout")]).indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
+ const afield = this.props.A.props.LayoutTemplateString?.indexOf("anchor1") === -1 ? "anchor2" : "anchor1";
const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
// really hacky stuff to make the LinkAnchorBox display where we want it to:
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 4b5e977df..0873fd1bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -10,6 +10,7 @@ import React = require("react");
import { Utils, emptyFunction } from "../../../../Utils";
import { SelectionManager } from "../../../util/SelectionManager";
import { DocumentType } from "../../../documents/DocumentTypes";
+import { DragManager } from "../../../util/DragManager";
@observer
export class CollectionFreeFormLinksView extends React.Component {
@@ -30,13 +31,13 @@ export class CollectionFreeFormLinksView extends React.Component {
return drawnPairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]);
return connections.filter(c =>
- c.a.props.layoutKey && c.b.props.layoutKey && c.a.props.Document.type === DocumentType.LINK &&
+ c.a.props.Document.type === DocumentType.LINK &&
c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed
).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
}
render() {
- return SelectionManager.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container">
+ return DragManager.Vals.Instance.GetIsDragging() ? (null) : <div className="collectionfreeformlinksview-container">
<svg className="collectionfreeformlinksview-svgCanvas">
{this.uniqueConnections}
</svg>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 28b461313..2aa9b1f5f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,13 +1,13 @@
import { library } from "@fortawesome/fontawesome-svg-core";
-import { faEye } from "@fortawesome/free-regular-svg-icons";
+import { faEye, faEyeSlash } 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, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc";
-import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
+import { documentSchema, collectionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkData, InkField, InkTool } from "../../../../new_fields/InkField";
+import { InkData, InkField, InkTool, PointData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
import { RichTextField } from "../../../../new_fields/RichTextField";
import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema";
@@ -32,7 +32,7 @@ import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionDockingView } from "../CollectionDockingView";
@@ -44,6 +44,7 @@ import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
import { CollectionViewType } from "../CollectionView";
+import { Timeline } from "../../animationtimeline/Timeline";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -65,11 +66,10 @@ export const panZoomSchema = createSchema({
fitH: "number"
});
-type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
-const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof collectionSchema, typeof documentSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentSchema, pageSchema);
export type collectionFreeformViewProps = {
forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
- childClickScript?: ScriptField;
viewDefDivClick?: ScriptField;
};
@@ -94,6 +94,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
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[])[] = [];
+ @observable _timelineRef = React.createRef<Timeline>();
@computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }
@computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; }
@@ -853,9 +854,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
@computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); }
- backgroundHalo = () => BoolCast(this.Document.useClusters);
+ @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); }
@computed get backgroundActive() { return this.layoutDoc.isBackground && (this.props.ContainingCollectionView?.active() || this.props.active()); }
- parentActive = () => this.props.active() || this.backgroundActive ? true : false;
+ backgroundHalo = () => BoolCast(this.Document.useClusters);
+ parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.backgroundActive ? true : false;
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
...this.props,
@@ -865,12 +867,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
DataDoc: childData,
Document: childLayout,
LibraryPath: this.libraryPath,
+ LayoutTemplate: this.props.ChildLayoutTemplate,
+ LayoutTemplateString: this.props.ChildLayoutString,
FreezeDimensions: this.props.freezeChildDimensions,
layoutKey: undefined,
+ setupDragLines: this.setupDragLines,
rootSelected: childData ? this.rootSelected : returnFalse,
dropAction: StrCast(this.props.Document.childDropAction) as dropActionType,
- //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,
+ onDoubleClick: this.onChildDoubleClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
PanelWidth: childLayout[WidthSym],
@@ -994,7 +999,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
- childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
get doLayoutComputation() {
const { newPool, computedElementData } = this.doInternalLayoutComputation;
runInAction(() =>
@@ -1020,7 +1024,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
replica={entry[1].replica}
dataProvider={this.childDataProvider}
sizeProvider={this.childSizeProvider}
- LayoutDoc={this.childLayoutDocFunc}
pointerEvents={
this.backgroundActive ?
true :
@@ -1037,7 +1040,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
componentDidMount() {
- super.componentDidMount();
+ super.componentDidMount?.();
this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
(elements) => this._layoutElements = elements || [],
{ fireImmediately: true, name: "doLayout" });
@@ -1126,10 +1129,60 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
input.click();
}
});
-
ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
+
+
+ ContextMenu.Instance.addItem({
+ description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => {
+ this._timelineVisible = !this._timelineVisible;
+ }), icon: this._timelineVisible ? faEyeSlash : faEye
+ });
+ }
+ @observable _timelineVisible = false;
+
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
}
+ @action
+ setupDragLines = () => {
+ const size = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight());
+ const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] };
+ const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) });
+ const isDocInView = (doc: Doc, rect: { left: number, top: number, width: number, height: number }) => {
+ if (this.intersectRect(docDims(doc), rect)) {
+ snappableDocs.push(doc);
+ }
+ };
+ const snappableDocs: Doc[] = []; // the set of documents in the visible viewport that we will try to snap to;
+ const otherBounds = { left: this.panX(), top: this.panY(), width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ this.getActiveDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to
+ !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z === undefined).map(doc => isDocInView(doc, selRect)); // if not, see if there are background docs to snap to
+ !snappableDocs.length && this.getActiveDocuments().filter(doc => doc.z !== undefined).map(doc => isDocInView(doc, otherBounds)); // if not, then why not snap to floating docs
+
+ const horizLines: number[] = [];
+ const vertLines: number[] = [];
+ snappableDocs.filter(doc => !DragManager.docsBeingDragged.includes(Cast(doc.rootDocument, Doc, null) || doc)).forEach(doc => {
+ const { left, top, width, height } = docDims(doc);
+ const topLeftInScreen = this.getTransform().inverse().transformPoint(left, top);
+ const docSize = this.getTransform().inverse().transformDirection(width, height);
+
+ horizLines.push(topLeftInScreen[1], topLeftInScreen[1] + docSize[1] / 2, topLeftInScreen[1] + docSize[1]); // horiz center line
+ vertLines.push(topLeftInScreen[0], topLeftInScreen[0] + docSize[0] / 2, topLeftInScreen[0] + docSize[0]);// right line
+ });
+ DragManager.SetSnapLines(horizLines, vertLines);
+ }
+ onPointerOver = (e: React.PointerEvent) => {
+ if (DragManager.Vals.Instance.GetIsDragging()) {
+ this.setupDragLines();
+ }
+ e.stopPropagation();
+ }
+
+ @observable private _hLines: number[] | undefined;
+ @observable private _vLines: number[] | undefined;
+
private childViews = () => {
const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
return [
@@ -1168,6 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
{this.children}
</CollectionFreeFormViewPannableContents>
+ {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
</MarqueeView>;
}
@@ -1179,7 +1233,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale ? wscale : hscale;
}
- @computed get backgroundEvents() { return this.layoutDoc.isBackground && SelectionManager.GetIsDragging(); }
+ @computed get backgroundEvents() { return this.layoutDoc.isBackground && DragManager.Vals.Instance.GetIsDragging(); }
render() {
TraceMobx();
const clientRect = this._mainCont?.getBoundingClientRect();
@@ -1192,7 +1246,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
return <div className={"collectionfreeformview-container"}
ref={this.createDashEventsTarget}
- onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
+ onPointerOver={this.onPointerOver}
+ onWheel={this.onPointerWheel} onClick={this.onClick} //pointerEvents: DragManager.Vals.Instance.GetIsDragging() ? "all" : undefined,
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu}
style={{
pointerEvents: this.backgroundEvents ? "all" : undefined,
@@ -1216,7 +1271,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}}>
</div>
-
+ {// uncomment to show snap lines
+ <div className="snapLines" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
+ <svg style={{ width: "100%", height: "100%" }}>
+ {this._hLines?.map(l => <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />)}
+ {this._vLines?.map(l => <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />)}
+ </svg>
+ </div>}
</div >;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cd8166309..5bac075b4 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,7 @@ import React = require("react");
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { RichTextField } from "../../../../new_fields/RichTextField";
import { CollectionView } from "../CollectionView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { ScriptField } from "../../../../new_fields/ScriptField";
interface MarqueeViewProps {
@@ -113,7 +113,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (template instanceof Doc) {
tbox._width = NumCast(template._width);
tbox.layoutKey = "layout_" + StrCast(template.title);
- tbox[StrCast(tbox.layoutKey)] = template;
+ Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template;
}
this.props.addLiveTextDocument(tbox);
}
@@ -613,7 +613,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
render() {
return <div className="marqueeView"
- style={{ overflow: StrCast(this.props.Document.overflow), }}
+ style={{ overflow: StrCast(this.props.Document._overflow), }}
onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}>
{this._visible ? this.marqueeDiv : null}
{this.props.children}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 9d09ecc3b..38b16e184 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -14,7 +14,7 @@ import "./collectionMulticolumnView.scss";
import ResizeBar from './MulticolumnResizer';
import WidthLabel from './MulticolumnWidthLabel';
import { List } from '../../../../new_fields/List';
-import { returnZero } from '../../../../Utils';
+import { returnZero, returnFalse, returnOne } from '../../../../Utils';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@@ -203,6 +203,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
@@ -215,9 +216,10 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return <ContentFittingDocumentView
Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
+ DataDoc={layout.resolvedDataDoc as Doc}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -225,21 +227,24 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
onClick={this.onChildClickHandler}
- getTransform={dxf}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>;
}
/**
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index af0cc3b5c..b55b67a9e 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ 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, returnZero } from '../../../../Utils';
+import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils';
import "./collectionMultirowView.scss";
import { computed, trace, observable, action } from 'mobx';
import { Transform } from '../../../util/Transform';
@@ -203,7 +203,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
-
+ @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); }
addDocTab = (doc: Doc, where: string) => {
if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) {
@@ -215,9 +215,10 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return <ContentFittingDocumentView
Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
+ DataDoc={layout.resolvedDataDoc as Doc}
backgroundColor={this.props.backgroundColor}
- LayoutDoc={this.props.childLayoutTemplate}
+ LayoutTemplate={this.props.ChildLayoutTemplate}
+ LayoutTemplateString={this.props.ChildLayoutString}
LibraryPath={this.props.LibraryPath}
FreezeDimensions={this.props.freezeChildDimensions}
renderDepth={this.props.renderDepth + 1}
@@ -225,21 +226,24 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
PanelHeight={height}
NativeHeight={returnZero}
NativeWidth={returnZero}
- fitToBox={BoolCast(this.props.Document._freezeChildDimensions)}
+ fitToBox={false}
rootSelected={this.rootSelected}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
onClick={this.onChildClickHandler}
- getTransform={dxf}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={dxf}
focus={this.props.focus}
- CollectionDoc={this.props.CollectionView?.props.Document}
- CollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
- active={this.props.active}
+ parentActive={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.addDocTab}
pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>;
}
/**
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 928413a11..c97e1062d 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -9,7 +9,7 @@ import { DragManager, SetupDrag } from "../../util/DragManager";
import { LinkManager } from "../../util/LinkManager";
import { DocumentView } from "../nodes/DocumentView";
import './LinkMenu.scss';
-import { LinkMenuItem } from "./LinkMenuItem";
+import { LinkMenuItem, StartLinkTargetsDrag } from "./LinkMenuItem";
import React = require("react");
interface LinkMenuGroupProps {
@@ -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, this.props.docView, e.x, e.y, this.props.sourceDoc, targets);
+ StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets);
}
e.stopPropagation();
}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index d091e06ef..d4e58fa8e 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -3,7 +3,7 @@ import { faArrowRight, faChevronDown, faChevronUp, faEdit, faEye, faTimes } from
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { Doc } from '../../../new_fields/Doc';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
import { Cast, StrCast } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
@@ -26,6 +26,41 @@ interface LinkMenuItemProps {
addDocTab: (document: Doc, where: string) => boolean;
}
+// drag links and drop link targets (aliasing them if needed)
+export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
+ const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+
+ if (draggedDocs.length) {
+ const moddrag: Doc[] = [];
+ for (const draggedDoc of draggedDocs) {
+ const doc = await Cast(draggedDoc.annotationOn, Doc);
+ if (doc) moddrag.push(doc);
+ }
+
+ const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
+ dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => {
+ docView.props.removeDocument?.(doc);
+ addDocument(doc);
+ return true;
+ };
+ const containingView = docView.props.ContainingCollectionView;
+ const finishDrag = (e: DragManager.DragCompleteEvent) =>
+ e.docDragData && (e.docDragData.droppedDocuments =
+ dragData.draggedDocuments.reduce((droppedDocs, d) => {
+ const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView);
+ if (dvs.length) {
+ dvs.forEach(dv => droppedDocs.push(dv.props.Document));
+ } else {
+ droppedDocs.push(Doc.MakeAlias(d));
+ }
+ return droppedDocs;
+ }, [] as Doc[]));
+
+ DragManager.StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag);
+ }
+}
+
+
@observer
export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _drag = React.createRef<HTMLDivElement>();
@@ -83,7 +118,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
this._eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
- DragManager.StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
+ StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
}
e.stopPropagation();
}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6ff6d1b42..1c5e13620 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -17,10 +17,10 @@ import { ContextMenu } from "../ContextMenu";
import { Id } from "../../../new_fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
-import { Docs } from "../../documents/Documents";
+import { Docs, DocUtils } from "../../documents/Documents";
import { ComputedField } from "../../../new_fields/ScriptField";
import { Networking } from "../../Network";
-import { Upload } from "../../../server/SharedMediaTypes";
+import { LinkAnchorBox } from "./LinkAnchorBox";
// testing testing
@@ -56,7 +56,6 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
@computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
set audioState(value) { this.dataDoc.audioState = value; }
public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); };
- public static ActiveRecordings: Doc[] = [];
@computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
@@ -144,7 +143,7 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
- AudioBox.ActiveRecordings.push(this.props.Document);
+ DocUtils.ActiveRecordings.push(this.props.Document);
this._recorder.ondataavailable = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(e.data);
if (!(result instanceof Error)) {
@@ -171,8 +170,8 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
this.audioState = "paused";
this._stream?.getAudioTracks()[0].stop();
- const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
- ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
+ const ind = DocUtils.ActiveRecordings.indexOf(this.props.Document);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
});
recordClick = (e: React.MouseEvent) => {
@@ -266,7 +265,8 @@ export class AudioBox extends ViewBoxBaseComponent<FieldViewProps, AudioDocument
NativeHeight={returnZero}
NativeWidth={returnZero}
rootSelected={returnFalse}
- layoutKey={Doc.LinkEndpoint(l, la2)}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(l, la2)}`)}
ContainingCollectionDoc={this.props.Document}
parentActive={returnTrue}
bringToFront={emptyFunction}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 1c7d116c5..cdbe506a5 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -7,20 +7,15 @@ import { DocComponent } from "../DocComponent";
import "./CollectionFreeFormDocumentView.scss";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { PositionDocument } from "../../../new_fields/documentSchemas";
+import { Document } from "../../../new_fields/documentSchemas";
import { TraceMobx } from "../../../new_fields/util";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, highlight?: boolean, z: number, transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined;
- x?: number;
- y?: number;
- z?: number;
zIndex?: number;
highlight?: boolean;
- width?: number;
- height?: number;
jitterRotation: number;
transition?: string;
fitToBox?: boolean;
@@ -28,7 +23,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
@observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, Document>(Document) {
@observable _animPos: number[] | undefined = undefined;
random(min: number, max: number) { // min should not be equal to max
const mseed = Math.abs(this.X * this.Y);
@@ -38,13 +33,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
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(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
- get X() { return 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.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+ get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
+ get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); }
get Highlight() { return this.dataProvider?.highlight; }
- get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); }
+ get width() { return this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); }
get height() {
- const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.height : this.layoutDoc[HeightSym]();
+ const hgt = this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.height : this.layoutDoc[HeightSym]();
return (hgt === undefined && this.nativeWidth && this.nativeHeight) ? this.width * this.nativeHeight / this.nativeWidth : hgt;
}
@computed get freezeDimensions() { return this.props.FreezeDimensions; }
@@ -75,10 +70,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox && !this.freezeDimensions ? this.width / this.nativeWidth : 1;
panelWidth = () => (this.sizeProvider?.width || this.props.PanelWidth?.());
panelHeight = () => (this.sizeProvider?.height || this.props.PanelHeight?.());
- getTransform = (): Transform => this.props.ScreenToLocalTransform()
- .translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling())
-
+ getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y).scale(1 / this.contentScaling());
focusDoc = (doc: Doc) => this.props.focus(doc, false);
NativeWidth = () => this.nativeWidth;
NativeHeight = () => this.nativeHeight;
@@ -115,13 +107,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight} />
: <ContentFittingDocumentView {...this.props}
- CollectionDoc={this.props.ContainingCollectionDoc}
- DataDocument={this.props.DataDoc}
- getTransform={this.getTransform}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ DataDoc={this.props.DataDoc}
+ ScreenToLocalTransform={this.getTransform}
NativeHeight={this.NativeHeight}
NativeWidth={this.NativeWidth}
- active={this.props.parentActive}
- focus={this.focusDoc}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
/>}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 814f8fd9c..7e45e8fcf 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -3,16 +3,16 @@ import { computed } from "mobx";
import { observer } from "mobx-react";
import "react-table/react-table.css";
import { Doc, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { NumCast, StrCast } from "../../../new_fields/Types";
+import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
import { TraceMobx } from "../../../new_fields/util";
import { emptyFunction, returnOne } from "../../../Utils";
-import { Transform } from "../../util/Transform";
-import { CollectionView } from "../collections/CollectionView";
import '../DocumentDecorations.scss';
-import { DocumentView } from "../nodes/DocumentView";
+import { DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import "./ContentFittingDocumentView.scss";
import { dropActionType } from "../../util/DragManager";
+import { CollectionView } from "../collections/CollectionView";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Transform } from "nodemailer/lib/xoauth2";
interface ContentFittingDocumentViewProps {
Document: Doc;
@@ -47,9 +47,13 @@ interface ContentFittingDocumentViewProps {
}
@observer
-export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
+export class ContentFittingDocumentView extends React.Component<DocumentViewProps>{
public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
- private get layoutDoc() { return this.props.LayoutDoc?.() || Doc.Layout(this.props.Document); }
+ private get layoutDoc() {
+ return this.props.LayoutTemplate?.() ||
+ (this.props.layoutKey && Doc.Layout(this.props.Document, Cast(this.props.Document[this.props.layoutKey], Doc, null))) ||
+ Doc.Layout(this.props.Document);
+ }
@computed get freezeDimensions() { return this.props.FreezeDimensions; }
nativeWidth = () => NumCast(this.layoutDoc?._nativeWidth, this.props.NativeWidth?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[WidthSym]() : this.props.PanelWidth()));
nativeHeight = () => NumCast(this.layoutDoc?._nativeHeight, this.props.NativeHeight?.() || (this.freezeDimensions && this.layoutDoc ? this.layoutDoc[HeightSym]() : this.props.PanelHeight()));
@@ -68,7 +72,7 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
@computed get panelWidth() { return this.nativeWidth && !this.props.Document._fitWidth ? this.nativeWidth() * this.contentScaling() : this.props.PanelWidth(); }
@computed get panelHeight() { return this.nativeHeight && !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 getTransform = () => this.props.ScreenToLocalTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
private get centeringOffset() { return this.nativeWidth() && !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; }
@@ -91,8 +95,9 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
}}>
<DocumentView {...this.props}
Document={this.props.Document}
- DataDoc={this.props.DataDocument}
- LayoutDoc={this.props.LayoutDoc}
+ DataDoc={this.props.DataDoc}
+ LayoutTemplate={this.props.LayoutTemplate}
+ LayoutTemplateString={this.props.LayoutTemplateString}
LibraryPath={this.props.LibraryPath}
NativeWidth={this.nativeWidth}
NativeHeight={this.nativeHeight}
@@ -108,11 +113,11 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
whenActiveChanged={this.props.whenActiveChanged}
- ContainingCollectionView={this.props.CollectionView}
- ContainingCollectionDoc={this.props.CollectionDoc}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
- parentActive={this.props.active}
+ parentActive={this.props.parentActive}
ScreenToLocalTransform={this.getTransform}
renderDepth={this.props.renderDepth}
focus={this.props.focus || emptyFunction}
diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocumentBox.scss
index ce21391ce..3a27c16c1 100644
--- a/src/client/views/nodes/DocumentBox.scss
+++ b/src/client/views/nodes/DocumentBox.scss
@@ -2,7 +2,8 @@
width: 100%;
height: 100%;
pointer-events: all;
- background: gray;
+ background: rgb(241, 239, 235);
+ position: absolute;
.documentBox-lock {
margin: auto;
color: white;
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
index 0d18baaed..b53c7cfe6 100644
--- a/src/client/views/nodes/DocumentBox.tsx
+++ b/src/client/views/nodes/DocumentBox.tsx
@@ -1,34 +1,38 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { IReactionDisposer, reaction, computed } from "mobx";
+import { action, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import { Doc, Field } from "../../../new_fields/Doc";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import { collectionSchema, documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { ComputedField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyPath } from "../../../Utils";
+import { TraceMobx } from "../../../new_fields/util";
+import { emptyPath, returnFalse, returnOne, returnZero } from "../../../Utils";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { DragManager } from "../../util/DragManager";
+import { undoBatch } from "../../util/UndoManager";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent } from "../DocComponent";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
import "./DocumentBox.scss";
+import { DocumentView } from "./DocumentView";
import { FieldView, FieldViewProps } from "./FieldView";
import React = require("react");
-import { TraceMobx } from "../../../new_fields/util";
-import { DocumentView } from "./DocumentView";
-import { Docs } from "../../documents/Documents";
-type DocHolderBoxSchema = makeInterface<[typeof documentSchema]>;
-const DocHolderBoxDocument = makeInterface(documentSchema);
+type DocHolderBoxSchema = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
+const DocHolderBoxDocument = makeInterface(documentSchema, collectionSchema);
@observer
export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, DocHolderBoxSchema>(DocHolderBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocHolderBox, fieldKey); }
_prevSelectionDisposer: IReactionDisposer | undefined;
+ _dropDisposer?: DragManager.DragDropDisposer;
_selections: Doc[] = [];
+ _contRef = React.createRef<HTMLDivElement>();
_curSelection = -1;
componentDidMount() {
- this._prevSelectionDisposer = reaction(() => this.contentDoc[this.props.fieldKey], (data) => {
+ this._prevSelectionDisposer = reaction(() => this.layoutDoc[this.props.fieldKey], (data) => {
if (data instanceof Doc && !this.isSelectionLocked()) {
this._selections.indexOf(data) !== -1 && this._selections.splice(this._selections.indexOf(data), 1);
this._selections.push(data);
@@ -42,22 +46,20 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
funcs.push({ description: (this.isSelectionLocked() ? "Show" : "Lock") + " Selection", event: () => this.toggleLockSelection, icon: "expand-arrows-alt" });
- funcs.push({ description: (this.props.Document.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => Doc.GetProto(this.props.Document).excludeCollections = !this.props.Document.excludeCollections, icon: "expand-arrows-alt" });
- funcs.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
+ funcs.push({ description: (this.layoutDoc.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => this.layoutDoc.excludeCollections = !this.layoutDoc.excludeCollections, icon: "expand-arrows-alt" });
+ funcs.push({ description: `${this.layoutDoc.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.layoutDoc.forceActive = !this.layoutDoc.forceActive, icon: "project-diagram" });
+ funcs.push({ description: `Show ${this.layoutDoc.childLayoutTemplateName !== "keyValue" ? "key values" : "contents"}`, event: () => this.layoutDoc.childLayoutString = this.layoutDoc.childLayoutString ? undefined : "<KeyValueBox {...props} />", icon: "project-diagram" });
ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
}
- @computed get contentDoc() {
- return (this.props.Document.isTemplateDoc || this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document));
- }
lockSelection = () => {
- this.contentDoc[this.props.fieldKey] = this.props.Document[this.props.fieldKey];
+ this.layoutDoc[this.props.fieldKey] = this.layoutDoc[this.props.fieldKey];
}
showSelection = () => {
- this.contentDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
+ this.layoutDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(self,this.excludeCollections,[_last_])?.[0]`);
}
isSelectionLocked = () => {
- const kvpstring = Field.toKeyValueString(this.contentDoc, this.props.fieldKey);
+ const kvpstring = Field.toKeyValueString(this.layoutDoc, this.props.fieldKey);
return !kvpstring || kvpstring.includes("DOC");
}
toggleLockSelection = () => {
@@ -67,13 +69,13 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
prevSelection = () => {
this.lockSelection();
if (this._curSelection > 0) {
- this.contentDoc[this.props.fieldKey] = this._selections[--this._curSelection];
+ this.layoutDoc[this.props.fieldKey] = this._selections[--this._curSelection];
return true;
}
}
nextSelection = () => {
if (this._curSelection < this._selections.length - 1 && this._selections.length) {
- this.contentDoc[this.props.fieldKey] = this._selections[++this._curSelection];
+ this.layoutDoc[this.props.fieldKey] = this._selections[++this._curSelection];
return true;
}
}
@@ -102,41 +104,70 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
e.stopPropagation();
}
}
- _contRef = React.createRef<HTMLDivElement>();
pwidth = () => this.props.PanelWidth() - 2 * this.xPad;
pheight = () => this.props.PanelHeight() - 2 * this.yPad;
getTransform = () => this.props.ScreenToLocalTransform().translate(-this.xPad, -this.yPad);
+ isActive = () => this.active() || !this.props.renderDepth;
+ layoutTemplateDoc = () => Cast(this.props.Document.childLayoutTemplate, Doc, null);
get renderContents() {
- const containedDoc = Cast(this.contentDoc[this.props.fieldKey], Doc, null);
- const childTemplateName = StrCast(this.props.Document.childTemplateName);
- if (containedDoc && childTemplateName && !containedDoc["layout_" + childTemplateName]) {
- setTimeout(() => {
- Doc.createCustomView(containedDoc, Docs.Create.StackingDocument, childTemplateName);
- Doc.expandTemplateLayout(Cast(containedDoc["layout_" + childTemplateName], Doc, null), containedDoc, undefined);
- }, 0);
- }
- const contents = !(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView
- Document={containedDoc}
- DataDocument={undefined}
- LibraryPath={emptyPath}
- CollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
- fitToBox={true}
- layoutKey={childTemplateName ? "layout_" + childTemplateName : "layout"}
- rootSelected={this.props.isSelected}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- removeDocument={this.props.removeDocument}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- getTransform={this.getTransform}
- renderDepth={this.props.renderDepth + 1}
- PanelWidth={this.pwidth}
- PanelHeight={this.pheight}
- focus={this.props.focus}
- active={this.props.active}
- dontRegisterView={!this.isSelectionLocked()}
- whenActiveChanged={this.props.whenActiveChanged}
- />;
+ const containedDoc = Cast(this.dataDoc[this.props.fieldKey], Doc, null);
+ const layoutTemplate = StrCast(this.layoutDoc.childLayoutString);
+ const contents = !(containedDoc instanceof Doc) ? (null) : this.layoutDoc.childLayoutString || this.layoutTemplateDoc() ?
+ <DocumentView
+ Document={containedDoc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
+ ContainingCollectionDoc={undefined}
+ fitToBox={true}
+ LayoutTemplateString={layoutTemplate}
+ LayoutTemplate={this.layoutTemplateDoc}
+ rootSelected={this.props.isSelected}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={this.pwidth}
+ PanelHeight={this.pheight}
+ focus={this.props.focus}
+ parentActive={this.isActive}
+ dontRegisterView={true}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne} /> :
+ <ContentFittingDocumentView
+ Document={containedDoc}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ ContainingCollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
+ ContainingCollectionDoc={undefined}
+ fitToBox={true}
+ LayoutTemplateString={layoutTemplate}
+ LayoutTemplate={this.layoutTemplateDoc}
+ rootSelected={this.props.isSelected}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={this.pwidth}
+ PanelHeight={this.pheight}
+ focus={this.props.focus}
+ parentActive={this.isActive}
+ dontRegisterView={true}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ />;
return contents;
}
render() {
@@ -145,16 +176,32 @@ export class DocHolderBox extends ViewBoxAnnotatableComponent<FieldViewProps, Do
onContextMenu={this.specificContextMenu}
onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
- background: StrCast(this.props.Document.backgroundColor),
+ background: StrCast(this.layoutDoc.backgroundColor),
border: `#00000021 solid ${this.xPad}px`,
borderTop: `#0000005e solid ${this.yPad}px`,
borderBottom: `#0000005e solid ${this.yPad}px`,
}}>
{this.renderContents}
- <div className="documentBox-lock" onClick={this.onLockClick}
+ <div className="documentBox-lock" onClick={this.onLockClick} ref={this.createDropTarget}
style={{ marginTop: - this.yPad }}>
<FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" />
</div>
</div >;
}
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ if (de.complete.docDragData.draggedDocuments[0].type === DocumentType.FONTICON) {
+ const doc = Cast(de.complete.docDragData.draggedDocuments[0].dragFactory, Doc, null);
+ this.props.Document.childLayoutTemplate = doc;
+ }
+ }
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this._dropDisposer?.();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
+ }
+
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index cd78ac7b3..81667e549 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,7 +19,7 @@ import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FontIconBox } from "./FontIconBox";
import { FieldView, FieldViewProps } from "./FieldView";
-import { FormattedTextBox } from "./FormattedTextBox";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
@@ -77,11 +77,11 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
render() {
const style: { [key: string]: any } = {};
const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "Document", "key", "onInput", "onClick", "__proto__"]).omit;
+ const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || "";
+ };
Object.keys(divKeys).map((prop: string) => {
const p = (this.props as any)[prop] as string;
- const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
- return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || "";
- };
style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
});
const Tag = this.props.htmltag as keyof JSX.IntrinsicElements;
@@ -96,8 +96,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
layoutKey: string,
- forceLayout?: string,
- forceFieldKey?: string,
hideOnLeave?: boolean,
makeLink?: () => Opt<Doc>, // function to call when a link is made
}> {
@@ -105,6 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
TraceMobx();
if (!this.layoutDoc) return "<p>awaiting layout</p>";
// const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string"); // bcz: replaced this with below... is it right?
+ if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, "layout")], "string");
if (this.props.layoutKey === "layout_keyValue") {
return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data"));
@@ -127,8 +126,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
get layoutDoc() {
const params = StrCast(this.props.Document.PARAMS);
// bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script
- // const template: Doc = this.props.LayoutDoc?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
- const template: Doc = this.props.LayoutDoc?.() ||
+ // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ const template: Doc = this.props.LayoutTemplate?.() || (this.props.LayoutTemplateString && this.props.Document) ||
(this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) ||
Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
return Doc.expandTemplateLayout(template, this.props.Document, params ? "(" + params + ")" : this.props.layoutKey);
@@ -186,25 +185,22 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
// layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc) ? (null) :
- this.props.forceLayout === "FormattedTextBox" && this.props.forceFieldKey ?
- <FormattedTextBox {...bindings.props} fieldKey={this.props.forceFieldKey} />
- :
- <ObserverJsxParser
- key={42}
- blacklistedAttrs={[]}
- renderInWrapper={false}
- components={{
- FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView,
- CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
- ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox,
- RecommendationsBox, ScreenshotBox, HTMLtag
- }}
- bindings={bindings}
- jsx={layoutFrame}
- showWarnings={true}
-
- onError={(test: any) => { console.log(test); }}
- />;
+ <ObserverJsxParser
+ key={42}
+ blacklistedAttrs={[]}
+ renderInWrapper={false}
+ components={{
+ FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, SliderBox, FieldView,
+ CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
+ PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
+ ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, DocHolderBox, LinkBox, ScriptingBox,
+ RecommendationsBox, ScreenshotBox, HTMLtag
+ }}
+ bindings={bindings}
+ jsx={layoutFrame}
+ showWarnings={true}
+
+ onError={(test: any) => { console.log(test); }}
+ />;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c9a745809..dea09cb30 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -75,6 +75,7 @@
display: inline-block;
width: 100%;
height: 100%;
+ border-radius: inherit;
.documentView-styleContentWrapper {
width: 100%;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4b5fadd93..c4cd5978a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -5,22 +5,21 @@ import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { Document, PositionDocument } from '../../../new_fields/documentSchemas';
+import { Document } from '../../../new_fields/documentSchemas';
import { Id } from '../../../new_fields/FieldSymbols';
import { InkTool } from '../../../new_fields/InkField';
-import { RichTextField } from '../../../new_fields/RichTextField';
import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField';
import { ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { AudioField, ImageField, PdfField, VideoField } from '../../../new_fields/URLField';
+import { ImageField } from '../../../new_fields/URLField';
import { TraceMobx } from '../../../new_fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
import { emptyFunction, OmitKeys, returnOne, returnTransparent, Utils } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { ClientRecommender } from '../../ClientRecommender';
import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DocumentManager } from "../../util/DocumentManager";
@@ -42,6 +41,7 @@ import { InkingControl } from '../InkingControl';
import { KeyphraseQueryView } from '../KeyphraseQueryView';
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
+import { LinkAnchorBox } from './LinkAnchorBox';
import { RadialMenu } from './RadialMenu';
import React = require("react");
@@ -58,12 +58,14 @@ export interface DocumentViewProps {
NativeHeight: () => number;
Document: Doc;
DataDoc?: Doc;
- LayoutDoc?: () => Opt<Doc>;
+ LayoutTemplateString?: string;
+ LayoutTemplate?: () => Opt<Doc>;
LibraryPath: Doc[];
fitToBox?: boolean;
contextMenuItems?: () => { script: ScriptField, label: string }[];
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
onClick?: ScriptField;
+ onDoubleClick?: ScriptField;
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
dropAction?: dropActionType;
@@ -73,6 +75,7 @@ export interface DocumentViewProps {
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
+ setupDragLines?: () => void;
renderDepth: number;
ContentScaling: () => number;
PanelWidth: () => number;
@@ -116,6 +119,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get nativeWidth() { return NumCast(this.layoutDoc._nativeWidth, this.props.NativeWidth() || (this.freezeDimensions ? this.layoutDoc[WidthSym]() : 0)); }
@computed get nativeHeight() { return NumCast(this.layoutDoc._nativeHeight, this.props.NativeHeight() || (this.freezeDimensions ? this.layoutDoc[HeightSym]() : 0)); }
@computed get onClickHandler() { return this.props.onClick || Cast(this.layoutDoc.onClick, ScriptField, null) || this.Document.onClick; }
+ @computed get onDoubleClickHandler() { return this.props.onDoubleClick || Cast(this.layoutDoc.onDoubleClick, ScriptField, null) || this.Document.onDoubleClick; }
@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; }
NativeWidth = () => this.nativeWidth;
@@ -289,13 +293,22 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
!this.props.Document.isBackground && this.props.bringToFront(this.props.Document);
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 (!(e.nativeEvent as any).formattedHandled) {
- const fullScreenAlias = Doc.MakeAlias(this.props.Document);
- if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
- fullScreenAlias.layoutKey = "layout_fullScreen";
+ if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
+ const func = () => this.onDoubleClickHandler.script.run({
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ thisContainer: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+ }, console.log);
+ func();
+ } else {
+ const fullScreenAlias = Doc.MakeAlias(this.props.Document);
+ if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
+ fullScreenAlias.layoutKey = "layout_fullScreen";
+ }
+ UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap");
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
}
- UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap");
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
}
} else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes("ScriptingBox")) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
//SelectionManager.DeselectAll();
@@ -441,8 +454,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const dY = -1 * Math.sign(dH);
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = PositionDocument(this.props.Document);
- const layoutDoc = PositionDocument(Doc.Layout(this.props.Document));
+ const doc = Document(this.props.Document);
+ const layoutDoc = Document(Doc.Layout(this.props.Document));
let nwidth = layoutDoc._nativeWidth || 0;
let nheight = layoutDoc._nativeHeight || 0;
const width = (layoutDoc._width || 0);
@@ -653,7 +666,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
toggleBackground = (temporary: boolean): void => {
- this.Document.overflow = temporary ? "visible" : "hidden";
+ this.Document._overflow = temporary ? "visible" : "hidden";
this.Document.isBackground = !temporary ? !this.Document.isBackground : (this.Document.isBackground ? undefined : true);
this.Document.isBackground && this.props.bringToFront(this.Document, true);
}
@@ -938,9 +951,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._queries = kps.toString();
}
- onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
- onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
-
// does Document set a layout prop
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
@@ -974,13 +984,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get contents() {
TraceMobx();
return (<>
- <DocumentContentsView key={1} ContainingCollectionView={this.props.ContainingCollectionView}
+ <DocumentContentsView key={1}
+ ContainingCollectionView={this.props.ContainingCollectionView}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
NativeWidth={this.NativeWidth}
NativeHeight={this.NativeHeight}
Document={this.props.Document}
DataDoc={this.props.DataDoc}
- LayoutDoc={this.props.LayoutDoc}
+ LayoutTemplateString={this.props.LayoutTemplateString}
+ LayoutTemplate={this.props.LayoutTemplate}
makeLink={this.makeLink}
rootSelected={this.rootSelected}
dontRegisterView={this.props.dontRegisterView}
@@ -1010,7 +1022,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
</>
);
}
- linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
// used to decide whether a link anchor view should be created or not.
// if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
@@ -1038,12 +1049,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ContainingCollectionDoc={this.props.Document} // bcz: hack this.props.Document is not a collection Need a better prop for passing the containing document to the LinkAnchorBox
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
- layoutKey={this.linkEndpoint(d)}
ContentScaling={returnOne}
backgroundColor={returnTransparent}
removeDocument={this.hideLinkAnchor}
pointerEvents={false}
- LayoutDoc={undefined}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(d, this.props.Document)}`)}
/>);
}
@computed get innards() {
@@ -1059,11 +1070,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const showCaption = StrCast(this.layoutDoc._showCaption);
const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined;
const captionView = (!showCaption ? (null) :
- <div className="documentView-captionWrapper">
+ <div className="documentView-captionWrapper" style={{ backgroundColor: StrCast(this.layoutDoc["caption-backgroundColor"]), color: StrCast(this.layoutDoc["caption-color"]) }}>
<DocumentContentsView {...OmitKeys(this.props, ['children']).omit}
hideOnLeave={true}
- forceLayout={"FormattedTextBox"}
- forceFieldKey={showCaption}
+ layoutTemplateString={`<FormattedTextBox {...props} fieldKey={'${showCaption}'}/>`}
ContentScaling={this.childScaling}
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
@@ -1092,7 +1102,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@computed get ignorePointerEvents() {
return this.props.pointerEvents === false ||
- (this.Document.isBackground && !this.isSelected() && !SelectionManager.GetIsDragging()) ||
+ (this.Document.isBackground && !this.isSelected() && !DragManager.Vals.Instance.GetIsDragging()) ||
(this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
}
@undoBatch
@@ -1103,17 +1113,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
Doc.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
}
}
- @observable _animate = 0;
+ @observable _animateScalingTo = 0;
switchViews = action((custom: boolean, view: string) => {
- SelectionManager.SetIsDragging(true);
- this._animate = 0.1;
+ this._animateScalingTo = 0.1; // shrink doc
setTimeout(action(() => {
this.setCustomView(custom, view);
- this._animate = 1;
- setTimeout(action(() => {
- this._animate = 0;
- SelectionManager.SetIsDragging(false);
- }), 400);
+ this._animateScalingTo = 1; // expand it
+ setTimeout(action(() => this._animateScalingTo = 0), 400);
}), 400);
});
@@ -1133,11 +1139,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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}
onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+ // onPointerEnter={e => Doc.BrushDoc(this.props.Document)}
+ // onPointerLeave={e => Doc.BrushDoc(this.props.Document)}
+ onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ let entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this.ContentDiv) {
+ entered = true;
+ }
+ }
+ !entered && Doc.UnBrushDoc(this.props.Document);
+ })}
style={{
- transformOrigin: this._animate ? "center center" : undefined,
- transform: this._animate ? `scale(${this._animate})` : undefined,
- transition: !this._animate ? StrCast(this.Document.transition) : this._animate < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out",
+ transformOrigin: this._animateScalingTo ? "center center" : undefined,
+ transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
+ transition: !this._animateScalingTo ? StrCast(this.Document.transition) : this._animateScalingTo < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out",
pointerEvents: this.ignorePointerEvents ? "none" : undefined,
color: StrCast(this.layoutDoc.color, "inherit"),
outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a3790d38b..016d2a1ae 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,7 +50,13 @@ export interface FieldViewProps {
setVideoBox?: (player: VideoBox) => void;
ContentScaling: () => number;
ChromeHeight?: () => number;
- childLayoutTemplate?: () => Opt<Doc>;
+ // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
+ height?: number;
+ width?: number;
+ background?: string;
+ color?: string;
+ xMargin?: number;
+ yMargin?: number;
}
@observer
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2970674a2..39d7109b1 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -238,8 +238,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
openItems.push({
description: "Default Perspective", event: () => {
- this.props.addDocTab(this.fieldDocToLayout, "inTab");
this.props.addDocTab(this.props.Document, "close");
+ this.props.addDocTab(this.fieldDocToLayout, "onRight");
}, icon: "image"
});
!open && cm.addItem({ description: "Change Perspective...", subitems: openItems, icon: "external-link-alt" });
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 707b9d12a..bc36e056e 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -4,7 +4,7 @@ import { Doc, DocListCast } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, setupMoveUpEvents } from '../../../Utils';
+import { Utils, setupMoveUpEvents, emptyFunction } from '../../../Utils';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
import { ViewBoxBaseComponent } from "../DocComponent";
@@ -17,7 +17,6 @@ import { LinkEditor } from "../linking/LinkEditor";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { SelectionManager } from "../../util/SelectionManager";
import { TraceMobx } from "../../../new_fields/util";
-import { DocumentView } from "./DocumentView";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -40,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
@observable _forceOpen = false;
onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onPointerMove, () => { }, this.onClick, false);
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
}
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
@@ -63,9 +62,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
return false;
});
@action
- onClick = (e: PointerEvent) => {
- this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0);
- this._lastTap = Date.now();
+ onClick = (e: React.MouseEvent) => {
if ((e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton)) {
this.props.select(false);
}
@@ -82,6 +79,9 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
} else {
this._timeout && clearTimeout(this._timeout);
this._timeout = undefined;
+ this._doubleTap = false;
+ this.openLinkEditor(e);
+ e.stopPropagation();
}
}
@@ -130,7 +130,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps, LinkAnch
</div>
);
const small = this.props.PanelWidth() <= 1;
- return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} title={targetTitle} onContextMenu={this.specificContextMenu}
+ return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu}
ref={this._ref} style={{
background: c,
left: !small ? `calc(${x}% - 7.5px)` : undefined,
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index bccf0f291..6f18b1321 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -198,7 +198,6 @@
}
.pdfBox {
- pointer-events: none;
.pdfViewer-text {
.textLayer {
span {
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index 78c19f351..d48000e16 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -1,10 +1,10 @@
.presBox-cont {
position: absolute;
+ pointer-events: inherit;
z-index: 2;
box-shadow: #AAAAAA .2vw .2vw .4vw;
- bottom: 0;
width: 100%;
- min-width: 120px;
+ min-width: 20px;
height: 100%;
min-height: 41px;
letter-spacing: 2px;
@@ -17,17 +17,36 @@
width: 100%;
}
.presBox-buttons {
- padding: 10px;
width: 100%;
background: gray;
- padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
+ display: grid;
+ grid-column-end: 4;
+ grid-column-start: 1;
+ .presBox-viewPicker {
+ height: 25;
+ position: relative;
+ display: inline-block;
+ grid-column: 1/2;
+ min-width: 15px;
+ }
+ select {
+ background: #323232;
+ color: white;
+ }
.presBox-button {
margin-right: 2.5%;
margin-left: 2.5%;
- width: 20%;
+ height: 25px;
border-radius: 5px;
+ display: flex;
+ align-items: center;
+ background: #323232;
+ color: white;
+ svg {
+ margin: auto;
+ }
}
.collectionViewBaseChrome-viewPicker {
min-width: 50;
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 80d043db1..343e74c87 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,12 +1,10 @@
import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, DocCastAsync } from "../../../new_fields/Doc";
import { InkTool } from "../../../new_fields/InkField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { returnFalse } from "../../../Utils";
import { documentSchema } from "../../../new_fields/documentSchemas";
import { DocumentManager } from "../../util/DocumentManager";
@@ -18,16 +16,11 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./PresBox.scss";
import { ViewBoxBaseComponent } from "../DocComponent";
import { makeInterface } from "../../../new_fields/Schema";
-
-library.add(faArrowLeft);
-library.add(faArrowRight);
-library.add(faPlay);
-library.add(faStop);
-library.add(faHandPointLeft);
-library.add(faPlus);
-library.add(faTimes);
-library.add(faMinus);
-library.add(faEdit);
+import { List } from "../../../new_fields/List";
+import { Docs } from "../../documents/Documents";
+import { PrefetchProxy } from "../../../new_fields/Proxy";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Scripting } from "../../util/Scripting";
type PresBoxSchema = makeInterface<[typeof documentSchema]>;
const PresBoxDocument = makeInterface(documentSchema);
@@ -35,22 +28,37 @@ const PresBoxDocument = makeInterface(documentSchema);
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>(PresBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
- _childReaction: IReactionDisposer | undefined;
@observable _isChildActive = false;
- componentDidMount() {
- this.layoutDoc._forceRenderEngine = "timeline";
- this.layoutDoc._replacedChrome = "replaced";
- this._childReaction = reaction(() => this.childDocs.slice(), (children) => children.forEach((child, i) => child.presentationIndex = i), { fireImmediately: true });
- }
- componentWillUnmount() {
- this._childReaction?.();
- }
-
@computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); }
- @computed get currentIndex() { return NumCast(this.layoutDoc._itemIndex); }
+ @computed get currentIndex() { return NumCast(this.presElement?.currentIndex); }
+ @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); }
+ constructor(props: any) {
+ super(props);
+ if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations.
+ Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
+ title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data"
+ }));
+ // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement
+ // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent
+ // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data
+ // stored on each pres element.
+ (this.presElement as Doc).lookupField = ScriptField.MakeScript(
+ `if (field === 'indexInPres') return docList(container[container.presentationFieldKey]).indexOf(data);` +
+ "if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 50 : 46;" +
+ "return undefined;", { field: "string", data: Doc.name, container: Doc.name });
+ }
+ this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
+ }
- updateCurrentPresentation = action(() => Doc.UserDoc().activePresentation = this.rootDoc);
+ componentDidMount() {
+ this.rootDoc.presBox = this.rootDoc;
+ this.rootDoc._forceRenderEngine = "timeline";
+ this.rootDoc._replacedChrome = "replaced";
+ }
+ updateCurrentPresentation = () => Doc.UserDoc().activePresentation = this.rootDoc;
+ @undoBatch
+ @action
next = () => {
this.updateCurrentPresentation();
if (this.childDocs[this.currentIndex + 1] !== undefined) {
@@ -66,6 +74,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
}
+
+ @undoBatch
+ @action
back = () => {
this.updateCurrentPresentation();
const docAtCurrent = this.childDocs[this.currentIndex];
@@ -83,10 +94,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
- whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
- active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
- (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
-
/**
* This is the method that checks for the actions that need to be performed
* after the document has been presented, which involves 3 button options:
@@ -95,15 +102,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
showAfterPresented = (index: number) => {
this.updateCurrentPresentation();
this.childDocs.forEach((doc, ind) => {
+ const presTargetDoc = doc.presentationTargetDoc as Doc;
//the order of cases is aligned based on priority
- if (doc.hideTillShownButton && ind <= index) {
- (doc.presentationTargetDoc as Doc).opacity = 1;
+ if (doc.presHideTillShownButton && ind <= index) {
+ presTargetDoc.opacity = 1;
}
- if (doc.hideAfterButton && ind < index) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ if (doc.presHideAfterButton && ind < index) {
+ presTargetDoc.opacity = 0;
}
- if (doc.fadeButton && ind < index) {
- (doc.presentationTargetDoc as Doc).opacity = 0.5;
+ if (doc.presFadeButton && ind < index) {
+ presTargetDoc.opacity = 0.5;
}
});
}
@@ -117,15 +125,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
this.updateCurrentPresentation();
this.childDocs.forEach((key, ind) => {
//the order of cases is aligned based on priority
-
+ const presTargetDoc = key.presentationTargetDoc as Doc;
if (key.hideAfterButton && ind >= index) {
- (key.presentationTargetDoc as Doc).opacity = 1;
+ presTargetDoc.opacity = 1;
}
if (key.fadeButton && ind >= index) {
- (key.presentationTargetDoc as Doc).opacity = 1;
+ presTargetDoc.opacity = 1;
}
if (key.hideTillShownButton && ind > index) {
- (key.presentationTargetDoc as Doc).opacity = 0;
+ presTargetDoc.opacity = 0;
}
});
}
@@ -151,11 +159,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
currentDocGroups.forEach((doc: Doc, index: number) => {
- if (doc.navButton) {
+ if (doc.presNavButton) {
docToJump = doc;
willZoom = false;
}
- if (doc.zoomButton) {
+ if (doc.presZoomButton) {
docToJump = doc;
willZoom = true;
}
@@ -166,10 +174,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const srcContext = aliasOf && await DocCastAsync(aliasOf.context);
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- const target = await DocCastAsync(curDoc.presentationTargetDoc);
- if (curDoc.navButton && target) {
+ const target = (await DocCastAsync(curDoc.presentationTargetDoc)) || curDoc;
+ if (curDoc.presNavButton && target) {
DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext);
- } else if (curDoc.zoomButton && target) {
+ } else if (curDoc.presZoomButton && target) {
//awaiting jump so that new scale can be found, since jumping is async
await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext);
}
@@ -180,22 +188,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
}
}
-
- @undoBatch
- public removeDocument = (doc: Doc) => {
- return Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
- }
-
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
public gotoDocument = (index: number, fromDoc: number) => {
this.updateCurrentPresentation();
Doc.UnBrushAllDocs();
if (index >= 0 && index < this.childDocs.length) {
- this.layoutDoc._itemIndex = index;
+ this.presElement.currentIndex = index;
- if (!this.layoutDoc.presStatus) {
- this.layoutDoc.presStatus = true;
+ if (!this.presElement.presStatus) {
+ this.presElement.presStatus = true;
this.startPresentation(index);
}
@@ -208,29 +210,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
//The function that starts or resets presentaton functionally, depending on status flag.
startOrResetPres = () => {
this.updateCurrentPresentation();
- if (this.layoutDoc.presStatus) {
+ if (this.presElement.presStatus) {
this.resetPresentation();
} else {
- this.layoutDoc.presStatus = true;
+ this.presElement.presStatus = true;
this.startPresentation(0);
this.gotoDocument(0, this.currentIndex);
}
}
- addDocument = (doc: Doc) => {
- const newPinDoc = Doc.MakeAlias(doc);
- newPinDoc.presentationTargetDoc = doc;
- return Doc.AddDocToList(this.dataDoc, this.fieldKey, newPinDoc);
- }
-
-
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
resetPresentation = () => {
this.updateCurrentPresentation();
this.childDocs.forEach(doc => (doc.presentationTargetDoc as Doc).opacity = 1);
- this.layoutDoc._itemIndex = 0;
- this.layoutDoc.presStatus = false;
+ this.rootDoc._itemIndex = 0;
+ this.presElement.presStatus = false;
}
//The function that starts the presentation, also checking if actions should be applied
@@ -238,88 +233,90 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
startPresentation = (startIndex: number) => {
this.updateCurrentPresentation();
this.childDocs.map(doc => {
- if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ const presTargetDoc = doc.presentationTargetDoc as Doc;
+ if (doc.presHideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
+ presTargetDoc.opacity = 0;
}
- if (doc.hideAfterButton && this.childDocs.indexOf(doc) < startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0;
+ if (doc.presHideAfterButton && this.childDocs.indexOf(doc) < startIndex) {
+ presTargetDoc.opacity = 0;
}
- if (doc.fadeButton && this.childDocs.indexOf(doc) < startIndex) {
- (doc.presentationTargetDoc as Doc).opacity = 0.5;
+ if (doc.presFadeButton && this.childDocs.indexOf(doc) < startIndex) {
+ presTargetDoc.opacity = 0.5;
}
});
}
- updateMinimize = undoBatch(action((e: React.ChangeEvent, mode: CollectionViewType) => {
+ updateMinimize = action((e: React.ChangeEvent, mode: CollectionViewType) => {
if (BoolCast(this.layoutDoc.inOverlay) !== (mode === CollectionViewType.Invalid)) {
if (this.layoutDoc.inOverlay) {
Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc);
CollectionDockingView.AddRightSplit(this.rootDoc);
this.layoutDoc.inOverlay = false;
} else {
- this.layoutDoc.x = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0];// 500;//e.clientX + 25;
- this.layoutDoc.y = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[1];////e.clientY - 25;
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ this.rootDoc.x = pt[0];// 500;//e.clientX + 25;
+ this.rootDoc.y = pt[1];////e.clientY - 25;
this.props.addDocTab?.(this.rootDoc, "close");
Doc.AddDocToList((Doc.UserDoc().myOverlayDocuments as Doc), undefined, this.rootDoc);
}
}
- }));
-
- initializeViewAliases = (docList: Doc[], viewtype: CollectionViewType) => {
- const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 46;
- docList.forEach(doc => {
- doc.presBox = this.rootDoc; // give contained documents a reference to the presentation
- doc.collapsedHeight = hgt; // set the collpased height for documents based on the type of view (Tree or Stack) they will be displaye din
- });
- }
-
- selectElement = (doc: Doc) => {
- this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.layoutDoc._itemIndex));
- }
-
- getTransform = () => {
- return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
- }
- panelHeight = () => {
- return this.props.PanelHeight() - 20;
- }
+ });
@undoBatch
viewChanged = action((e: React.ChangeEvent) => {
//@ts-ignore
- this.layoutDoc._viewType = e.target.selectedOptions[0].value;
- this.layoutDoc._viewType === CollectionViewType.Stacking && (this.layoutDoc._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- this.updateMinimize(e, StrCast(this.layoutDoc._viewType));
+ const viewType = e.target.selectedOptions[0].value as CollectionViewType;
+ viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
+ this.updateMinimize(e, this.rootDoc._viewType = viewType);
});
- childLayoutTemplate = () => this.layoutDoc._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc()["template-presentation"], Doc, null) : undefined;
+ whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
+ addDocumentFilter = (doc: Doc) => {
+ doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf);
+ !this.childDocs.includes(doc) && (doc.presZoomButton = true);
+ return true;
+ }
+ childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement;
+ removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
+ selectElement = (doc: Doc) => this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.rootDoc._itemIndex));
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
+ panelHeight = () => this.props.PanelHeight() - 20;
+ active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.layoutDoc.isBackground) &&
+ (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+
render() {
- const mode = StrCast(this.layoutDoc._viewType) as CollectionViewType;
- this.initializeViewAliases(this.childDocs, mode);
- return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined, pointerEvents: this.active() || this.layoutDoc.inOverlay ? "all" : "none" }} >
- <div className="presBox-buttons" style={{ display: this.layoutDoc._chromeStatus === "disabled" ? "none" : undefined }}>
- <select className="collectionViewBaseChrome-viewPicker"
+ this.rootDoc.presOrderedDocs = new List<Doc>(this.childDocs.map((child, i) => child));
+ const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ return <div className="presBox-cont" style={{ minWidth: this.layoutDoc.inOverlay ? 240 : undefined }} >
+ <div className="presBox-buttons" style={{ display: this.rootDoc._chromeStatus === "disabled" ? "none" : undefined }}>
+ <select className="presBox-viewPicker"
onPointerDown={e => e.stopPropagation()}
onChange={this.viewChanged}
value={mode}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
</select>
- <button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- <button className="presBox-button" title={"Reset Presentation" + this.layoutDoc.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
+ <div className="presBox-button" title="Back" style={{ gridColumn: 2 }} onClick={this.back}>
+ <FontAwesomeIcon icon={"arrow-left"} />
+ </div>
+ <div className="presBox-button" title={"Reset Presentation" + this.layoutDoc.presStatus ? "" : " From Start"} style={{ gridColumn: 3 }} onClick={this.startOrResetPres}>
<FontAwesomeIcon icon={this.layoutDoc.presStatus ? "stop" : "play"} />
- </button>
- <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ <div className="presBox-button" title="Next" style={{ gridColumn: 4 }} onClick={this.next}>
+ <FontAwesomeIcon icon={"arrow-right"} />
+ </div>
</div>
<div className="presBox-listCont" >
{mode !== CollectionViewType.Invalid ?
<CollectionView {...this.props}
+ ContainingCollectionDoc={this.props.Document}
+ PanelWidth={this.props.PanelWidth}
PanelHeight={this.panelHeight}
moveDocument={returnFalse}
childLayoutTemplate={this.childLayoutTemplate}
- addDocument={this.addDocument}
+ filterAddDocument={this.addDocumentFilter}
removeDocument={returnFalse}
focus={this.selectElement}
ScreenToLocalTransform={this.getTransform} />
@@ -328,4 +325,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>;
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function lookupPresBoxField(presLayout: Doc, data: Doc, fieldKey: string) {
+});
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
index 76885eada..d248b098c 100644
--- a/src/client/views/nodes/QueryBox.tsx
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -11,6 +11,7 @@ import { SearchBox } from "../search/SearchBox";
import { FieldView, FieldViewProps } from './FieldView';
import "./QueryBox.scss";
import { List } from "../../../new_fields/List";
+import { DragManager } from "../../util/DragManager";
type QueryDocument = makeInterface<[typeof documentSchema]>;
const QueryDocument = makeInterface(documentSchema);
@@ -27,7 +28,7 @@ export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryD
}
render() {
- const dragging = !SelectionManager.GetIsDragging() ? "" : "-dragging";
+ const dragging = !DragManager.Vals.Instance.GetIsDragging() ? "" : "-dragging";
return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} >
<SearchBox
id={this.props.Document[Id]}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 125690dc7..a0ecc9ff5 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
-import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
@@ -20,8 +20,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./ScreenshotBox.scss";
const path = require('path');
-type ScreenshotDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-const ScreenshotDocument = makeInterface(documentSchema, positionSchema);
+type ScreenshotDocument = makeInterface<[typeof documentSchema]>;
+const ScreenshotDocument = makeInterface(documentSchema);
library.add(faVideo);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 613929bca..266b7f43f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -21,14 +21,14 @@ import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
-import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { documentSchema } from "../../../new_fields/documentSchemas";
const path = require('path');
export const timeSchema = createSchema({
currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
});
-type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
-const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
+type VideoDocument = makeInterface<[typeof documentSchema, typeof timeSchema]>;
+const VideoDocument = makeInterface(documentSchema, timeSchema);
library.add(faVideo);
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
new file mode 100644
index 000000000..d94fe7fc6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -0,0 +1,95 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IDashDocCommentView {
+ node: any;
+ view: any;
+ getPos: any;
+}
+
+export class DashDocCommentView extends React.Component<IDashDocCommentView>{
+ constructor(props: IDashDocCommentView) {
+ super(props);
+ }
+
+ targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = this.props.getPos() + 1; i < this.props.view.state.doc.content.size; i++) {
+ const m = this.props.view.state.doc.nodeAt(i);
+ if (m && m.type === this.props.view.state.schema.nodes.dashDoc && m.attrs.docid === this.props.node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = this.props.view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.node.attrs.docid, float: "right" });
+ this.props.view.dispatch(this.props.view.state.tr.insert(this.props.getPos() + 1, dashDoc));
+ setTimeout(() => { try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ }
+
+ onPointerDownCollapse = (e: any) => e.stopPropagation();
+
+ onPointerUpCollapse = (e: any) => {
+ const target = this.targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ }
+
+ onPointerEnterCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ onPointerLeaveCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ render() {
+
+ const collapsedId = "DashDocCommentView-" + this.props.node.attrs.docid;
+
+ return (
+ <span
+ className="formattedTextBox-inlineComment"
+ id={collapsedId}
+ onPointerDown={this.onPointerDownCollapse}
+ onPointerUp={this.onPointerUpCollapse}
+ onPointerEnter={this.onPointerEnterCollapse}
+ onPointerLeave={this.onPointerLeaveCollapse}
+ >
+
+ </span >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
new file mode 100644
index 000000000..7130fee2b
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -0,0 +1,269 @@
+import { IReactionDisposer, reaction } from "mobx";
+import { NodeSelection } from "prosemirror-state";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+interface IDashDocView {
+ node: any;
+ view: any;
+ getPos: any;
+ tbox?: FormattedTextBox;
+ self: any;
+}
+
+export class DashDocView extends React.Component<IDashDocView> {
+
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+ _finalLayout: any;
+ _resolvedDataDoc: any;
+
+
+ // constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+
+ constructor(props: IDashDocView) {
+ super(props);
+
+ const node = this.props.node;
+ this._textBox = this.props.tbox as FormattedTextBox;
+
+ const alias = node.attrs.alias;
+ const docid = node.attrs.docid || this._textBox.props.Document[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.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ this._dashDoc = aliasedDoc;
+ // self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ this._dashDoc = dashDoc;
+ // self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+
+ this.onPointerLeave = this.onPointerLeave.bind(this);
+ this.onPointerEnter = this.onPointerEnter.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.onWheel = this.onWheel.bind(this);
+ }
+ /* #region Internal functions */
+
+ removeDoc = () => {
+ const view = this.props.view;
+ const pos = this.props.getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ }
+
+ getDocTransform = () => {
+ const outerElement = document.getElementById('dash-document-view-outer') as HTMLElement;
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(outerElement);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? 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
+
+ onKeyPress = (e: any) => {
+ e.stopPropagation();
+ }
+ onWheel = (e: any) => {
+ e.preventDefault();
+ }
+ onKeyUp = (e: any) => {
+ e.stopPropagation();
+ }
+ onKeyDown = (e: any) => {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ }
+ onPointerLeave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ }
+ onPointerEnter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ }
+ /*endregion*/
+
+ componentWillMount = () => {
+ this._reactionDisposer?.();
+ }
+
+ componentDidUpdate = () => {
+
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+
+ const dashDoc = this._dashDoc as Doc;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = this.props.node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, this.props.node.attrs.fieldKey);
+
+ if (finalLayout) {
+ if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ finalLayout.rootDocument = dashDoc.aliasOf;
+ }
+ 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[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ this._finalLayout = finalLayout;
+ this._resolvedDataDoc = Cast(finalLayout.resolvedDataDoc, Doc, null);
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ }
+ },
+ (res) => {
+
+ if (res) {
+ this._finalLayout = res.finalLayout;
+ this._resolvedDataDoc = res.resolvedDataDoc;
+
+ this.forceUpdate(); // doReactRender(res.finalLayout, res.resolvedDataDoc),
+ }
+ },
+ { fireImmediately: true });
+
+ }
+
+ render() {
+ // doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+
+ const node = this.props.node;
+ const view = this.props.view;
+ const getPos = this.props.getPos;
+
+ const spanStyle = {
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ position: 'absolute' as 'absolute',
+ display: 'inline-block'
+ };
+
+
+ const outerStyle = {
+ position: "relative" as "relative",
+ textIndent: "0",
+ border: "1px solid " + StrCast(this._textBox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")),
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ display: this.props.node.props.hidden ? "none" : "inline-block",
+ float: this.props.node.props.float,
+ };
+
+ const dashDoc = this._dashDoc as Doc;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+ const resolvedDataDoc = this._resolvedDataDoc; //Added this
+
+ if (!finalLayout) {
+ return <div></div>;
+ // if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ } else {
+
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() =>
+ ({
+ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()],
+ color: finalLayout.color
+ }),
+ ({ dim, color }) => {
+ spanStyle.width = outerStyle.width = Math.max(20, dim[0]) + "px";
+ spanStyle.height = outerStyle.height = Math.max(20, dim[1]) + "px";
+ outerStyle.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: 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 doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ // ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ return (
+ <span id="dash-document-view-outer"
+ className="outer"
+ style={outerStyle}
+ >
+ <div id="dashSpan"
+ className="dash-span"
+ style={spanStyle}
+ onPointerLeave={this.onPointerLeave}
+ onPointerEnter={this.onPointerEnter}
+ onKeyDown={this.onKeyDown}
+ onKeyPress={this.onKeyPress}
+ onKeyUp={this.onKeyUp}
+ onWheel={this.onWheel}
+ >
+ <DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={this.removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />
+
+ </div>
+ </span>
+ );
+
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
new file mode 100644
index 000000000..35ff9c1e6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -0,0 +1,36 @@
+.dashFieldView {
+ position: relative;
+ display: inline-block;
+
+ .dashFieldView-enumerables {
+ width: 10px;
+ height: 10px;
+ position: relative;
+ display: inline-block;
+ background: dimGray;
+ }
+ .dashFieldView-fieldCheck {
+ min-width: 12px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ }
+ .dashFieldView-labelSpan {
+ position: relative;
+ display: inline-block;
+ font-size: small;
+ }
+ .dashFieldView-fieldSpan {
+ min-width: 20px;
+ margin-left: 2px;
+ margin-right: 5px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ span {
+ min-width: 100%;
+ display: inline-block;
+ }
+ }
+}
+ \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
new file mode 100644
index 000000000..d87d6e424
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -0,0 +1,207 @@
+import { IReactionDisposer, observable, runInAction, computed, action } from "mobx";
+import { Doc, DocListCast, Field } from "../../../../new_fields/Doc";
+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, StrCast } from "../../../../new_fields/Types";
+import { DocServer } from "../../../DocServer";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import React = require("react");
+import * as ReactDOM from 'react-dom';
+import "./DashFieldView.scss";
+import { observer } from "mobx-react";
+
+
+export class DashFieldView {
+ _fieldWrapper: HTMLDivElement; // container for label and value
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._fieldWrapper = document.createElement("div");
+ this._fieldWrapper.style.width = node.attrs.width;
+ this._fieldWrapper.style.height = node.attrs.height;
+ this._fieldWrapper.style.fontWeight = "bold";
+ this._fieldWrapper.style.position = "relative";
+ this._fieldWrapper.style.display = "inline-block";
+ this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
+
+ ReactDOM.render(<DashFieldViewInternal
+ fieldKey={node.attrs.fieldKey}
+ docid={node.attrs.docid}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ tbox={tbox}
+ />, this._fieldWrapper);
+ (this as any).dom = this._fieldWrapper;
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._fieldWrapper);
+ }
+ selectNode() { }
+
+}
+interface IDashFieldViewInternal {
+ fieldKey: string;
+ docid: string;
+ tbox: FormattedTextBox;
+ width: number;
+ height: number;
+}
+
+@observer
+export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> {
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ _fieldKey: string;
+ _fieldStringRef = React.createRef<HTMLSpanElement>();
+ @observable _showEnumerables: boolean = false;
+ @observable _dashDoc: Doc | undefined;
+
+ constructor(props: IDashFieldViewInternal) {
+ super(props);
+ this._fieldKey = this.props.fieldKey;
+ this._textBoxDoc = this.props.tbox.props.Document;
+
+ if (this.props.docid) {
+ DocServer.GetRefField(this.props.docid).
+ then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ } else {
+ this._dashDoc = this.props.tbox.props.DataDoc || this.props.tbox.dataDoc;
+ }
+ }
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ }
+
+ // set the display of the field's value (checkbox for booleans, span of text for strings)
+ @computed get fieldValueContent() {
+ if (this._dashDoc) {
+ const dashVal = this._dashDoc[this._fieldKey];
+ const fval = StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal;
+ const boolVal = Cast(fval, "boolean", null);
+ const strVal = Field.toString(fval as Field) || "";
+
+ // field value is a boolean, so use a checkbox or similar widget to display it
+ if (boolVal === true || boolVal === false) {
+ return <input
+ className="dashFieldView-fieldCheck"
+ type="checkbox" checked={boolVal}
+ onChange={e => this._dashDoc![this._fieldKey] = e.target.checked}
+ />;
+ }
+ else // field value is a string, so display it as an editable span
+ {
+ // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't
+ // use React events. Essentially, React events occur after native events have been processed, so corresponding React events
+ // will never fire because Prosemirror has handled the native events. So we add listeners for native events here.
+ return <span contentEditable={true} suppressContentEditableWarning={true} defaultValue={strVal} ref={r => {
+ r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r));
+ r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false));
+ r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true));
+ }} >
+ {strVal}
+ </span>;
+ }
+ }
+ }
+
+ // we need to handle all key events on the input span or else they will propagate to prosemirror.
+ @action
+ fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
+ if (e.key === "Enter") { // handle the enter key by "submitting" the current text to Dash's database.
+ e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+ this.updateText(span.textContent!, true);
+ e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view
+ }
+ if (e.key === "a" && (e.ctrlKey || e.metaKey)) { // handle ctrl-A to select all the text within the span
+ if (window.getSelection) {
+ const range = document.createRange();
+ range.selectNodeContents(span);
+ window.getSelection()!.removeAllRanges();
+ window.getSelection()!.addRange(range);
+ }
+ e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
+ }
+ e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
+ }
+
+ @action
+ updateText = (nodeText: string, forceMatch: boolean) => {
+ this._showEnumerables = false;
+ if (nodeText) {
+ const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(this._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
+ Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
+ this._dashDoc![this._fieldKey] = modText;
+ } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ else if (nodeText.startsWith(":=")) {
+ this._dashDoc![this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
+ } else if (nodeText.startsWith("=:=")) {
+ Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
+ } else {
+ this._dashDoc![this._fieldKey] = newText;
+ }
+ });
+ }
+ }
+
+ // display a collection of all the enumerable values for this field
+ onPointerDownEnumerables = async (e: any) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
+ collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
+ }
+
+
+ // clicking on the label creates a pivot view collection of all documents
+ // in the same collection. The pivot field is the fieldKey of this label
+ onPointerDownLabelSpan = (e: any) => {
+ e.stopPropagation();
+ let container = this.props.tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = this._fieldKey;
+ this.props.tbox.props.addDocTab(alias, "onRight");
+ }
+ }
+
+ render() {
+ return <div className="dashFieldView" style={{
+ width: this.props.width,
+ height: this.props.height,
+ }}>
+ <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
+ {this._fieldKey}
+ </span>
+
+ <div className="dashFieldView-fieldSpan">
+ {this.fieldValueContent}
+ </div>
+
+ {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
+
+ </div >;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
new file mode 100644
index 000000000..ee21fb765
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -0,0 +1,162 @@
+import { EditorView } from "prosemirror-view";
+import { EditorState } from "prosemirror-state";
+import { keymap } from "prosemirror-keymap";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { schema } from "./schema_rts";
+import { redo, undo } from "prosemirror-history";
+import { StepMap } from "prosemirror-transform";
+
+import React = require("react");
+
+interface IFootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+}
+
+export class FootnoteView extends React.Component<IFootnoteView> {
+ _innerView: any;
+ _node: any;
+
+ constructor(props: IFootnoteView) {
+ super(props);
+ const node = this.props.node;
+ const outerView = this.props.outerView;
+ const _innerView = this.props.innerView;
+ const getPos = this.props.getPos;
+ }
+
+ selectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.props.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.props.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.props.innerView.defineProperty(new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.props.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-y": () => redo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.props.outerView.hasFocus()) this.props.innerView.focus();
+ }) as any
+ }
+ }));
+ setTimeout(() => this.props.innerView && this.props.innerView.docView.setSelection(0, 0, this.props.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.props.innerView.state.applyTransaction(tr);
+ this.props.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.props.outerView.state.tr, offsetMap = StepMap.offset(this.props.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.props.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.props.node)) return false;
+ this._node = node; //not sure
+ if (this.props.innerView) {
+ const state = this.props.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.props.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+ onPointerUp = (e: any) => {
+ this.toggle(e);
+ }
+
+ toggle = (e: any) => {
+ e.preventDefault();
+ if (this.props.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+
+ close() {
+ this.props.innerView && this.props.innerView.destroy();
+ this._innerView = null;
+ this.dom.textContent = "";
+ }
+
+ destroy() {
+ if (this.props.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.props.innerView && this.props.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+
+
+ render() {
+ return (
+ <div
+ className="footnote"
+ onPointerUp={this.onPointerUp}>
+ <div className="footnote-tooltip" >
+
+ </div >
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 3bedb7127..477a2ca08 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import "../globalCssVariables";
+@import "../../globalCssVariables";
.ProseMirror {
width: 100%;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 3d6ca66f0..658a55f51 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -12,49 +12,62 @@ import { Fragment, Mark, Node, Slice } from "prosemirror-model";
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
-import { DateField } from '../../../new_fields/DateField';
-import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { InkTool } from '../../../new_fields/InkField';
-import { PrefetchProxy } from '../../../new_fields/Proxy';
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { RichTextUtils } from '../../../new_fields/RichTextUtils';
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { TraceMobx } from '../../../new_fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../Utils';
-import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { DictationManager } from '../../util/DictationManager';
-import { DragManager } from "../../util/DragManager";
-import { makeTemplate } from '../../util/DropConverter';
-import buildKeymap from "../../util/ProsemirrorExampleTransfer";
-import RichTextMenu from '../../util/RichTextMenu';
-import { RichTextRules } from "../../util/RichTextRules";
-import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, ImageResizeView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema";
-import { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { DocumentButtonBar } from '../DocumentButtonBar';
-import { InkingControl } from "../InkingControl";
-import { AudioBox } from './AudioBox';
-import { FieldView, FieldViewProps } from "./FieldView";
+import { DateField } from '../../../../new_fields/DateField';
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { Id } from '../../../../new_fields/FieldSymbols';
+import { InkTool } from '../../../../new_fields/InkField';
+import { PrefetchProxy } from '../../../../new_fields/Proxy';
+import { RichTextField } from "../../../../new_fields/RichTextField";
+import { RichTextUtils } from '../../../../new_fields/RichTextUtils';
+import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { TraceMobx } from '../../../../new_fields/util';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } from '../../../../Utils';
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
+import { DocServer } from "../../../DocServer";
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DictationManager } from '../../../util/DictationManager';
+import { DragManager } from "../../../util/DragManager";
+import { makeTemplate } from '../../../util/DropConverter';
+import buildKeymap from "./ProsemirrorExampleTransfer";
+import RichTextMenu from './RichTextMenu';
+import { RichTextRules } from "./RichTextRules";
+import { DashDocCommentView, DashDocView, FootnoteView, ImageResizeView, OrderedListView, SummaryView } from "./RichTextSchema";
+// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "./RichTextSchema";
+// import { OrderedListView } from "./RichTextSchema";
+// import { ImageResizeView } from "./ImageResizeView";
+// import { DashDocCommentView } from "./DashDocCommentView";
+// import { FootnoteView } from "./FootnoteView";
+// import { SummaryView } from "./SummaryView";
+// import { DashDocView } from "./DashDocView";
+import { DashFieldView } from "./DashFieldView";
+
+import { schema } from "./schema_rts";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
+import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { ViewBoxAnnotatableComponent } from "../../DocComponent";
+import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { InkingControl } from "../../InkingControl";
+import { AudioBox } from '../AudioBox';
+import { FieldView, FieldViewProps } from "../FieldView";
import "./FormattedTextBox.scss";
import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
+import { ScriptField } from '../../../../new_fields/ScriptField';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
- hideOnLeave?: boolean;
- makeLink?: () => Opt<Doc>;
+ makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
+ hideOnLeave?: boolean; // used by DocumentView for setting caption's hide on leave (bcz: would prefer to have caption-hideOnLeave field set or something similar)
+ xMargin?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
+ yMargin?: number;
}
const richTextSchema = createSchema({
@@ -185,15 +198,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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 * 1000)));
const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
- const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
- if (!this._applyingChange) {
+ const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField); // the actual text in the text box
+ const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+ const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
+ const json = JSON.stringify(state.toJSON());
+ if (!this._applyingChange && json.replace(/"selection":.*/, "") !== curProto?.Data.replace(/"selection":.*/, "")) {
this._applyingChange = true;
this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
- if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), curText);
- this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ if ((!curTemp && !curProto) || curText) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
+ if (curText !== curLayout?.Text) {
+ this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
+ this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ }
} else { // if we've deleted all the text in a note driven by a template, then restore the template data
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data)));
+ this.dataDoc[this.props.fieldKey] = undefined;
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
}
this._applyingChange = false;
@@ -216,7 +235,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
public hyperlinkTerms = (terms: string[], target: Doc) => {
if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- let tr = this._editorView.state.tr;
+ const tr = this._editorView.state.tr;
const flattened: TextSelection[] = [];
res.map(r => r.map(h => flattened.push(h)));
const lastSel = Math.min(flattened.length - 1, this._searchIndex);
@@ -406,14 +425,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
- this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
+ this.rootDoc.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
- !this.props.Document.rootDocument && funcs.push({
+ !this.layoutDoc.isTemplateDoc && funcs.push({
description: "Make Template", event: () => {
- this.props.Document.isTemplateDoc = makeTemplate(this.props.Document);
+ this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc);
Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
}, icon: "eye"
});
+ this.layoutDoc.isTemplateDoc && funcs.push({
+ description: "Make New Template", event: () => {
+ const title = this.rootDoc.title as string;
+ this.rootDoc.layout = (this.layoutDoc as Doc).layout as string;
+ this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
+ this.rootDoc.isTemplateDoc = false;
+ this.rootDoc.isTemplateForField = "";
+ this.rootDoc.layoutKey = "layout";
+ this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
+ setTimeout(() => {
+ this.rootDoc._width = this.layoutDoc._width || 300; // the width and height are stored on the template, since we're getting rid of the old template
+ this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields
+ this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, "string", null);
+ }, 10);
+ Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc);
+ }, icon: "eye"
+ });
funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
@@ -867,7 +903,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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); },
+ // dashDoc(node, view, getPos) { return new DashDocView({ node, view, getPos, self }); },
+
+ // image(node, view, getPos) {
+ // //const addDocTab = this.props.addDocTab;
+ // return new ImageResizeView({ node, view, getPos, addDocTab: this.props.addDocTab });
+ // },
+ // // WAS :
+ // //image(node, view, getPos) { return new ImageResizeView(node, view, getPos, this.props.addDocTab); },
+
summary(node, view, getPos) { return new SummaryView(node, view, getPos); },
ordered_list(node, view, getPos) { return new OrderedListView(); },
footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
@@ -1178,11 +1222,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.layoutDoc._autoHeight = false;
}
const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
- const dh = NumCast(this.layoutDoc._height, 0);
+ const dh = NumCast(this.rootDoc._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;
+ if (this.rootDoc !== this.layoutDoc && !this.layoutDoc.resolvedDataDoc) {
+ // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
+ console.log("Delayed height adjustment...");
+ setTimeout(() => {
+ this.rootDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ }, 10);
+ } else {
+ this.rootDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ }
}
}
}
@@ -1193,6 +1246,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
render() {
TraceMobx();
+ const style: { [key: string]: any } = {};
+ const divKeys = ["width", "height", "background"];
+ const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
+ return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.rootDoc, this: this.layoutDoc }).result as string || "";
+ };
+ divKeys.map((prop: string) => {
+ const p = (this.props as any)[prop] as string;
+ p && (style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer));
+ });
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
if (this.props.isSelected()) {
@@ -1201,15 +1263,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
FormattedTextBoxComment.Hide();
}
return (
+
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
- background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]),
+ height: this.props.height ? this.props.height : this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
+ background: this.props.background ? this.props.background : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : ""),
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
- color: this.props.hideOnLeave ? "white" : "inherit",
+ color: this.props.color ? this.props.color : StrCast(this.layoutDoc[this.props.fieldKey + "-color"], this.props.hideOnLeave ? "white" : "inherit"),
pointerEvents: interactive ? "none" : undefined,
fontSize: Cast(this.layoutDoc._fontSize, "number", null),
fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
+ ...style
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
@@ -1222,12 +1286,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
- onPointerLeave={action(() => this._entered = false)}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ this._entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this._ref.current!) {
+ this._entered = true;
+ }
+ }
+ })}
>
<div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}>
<div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget}
style={{
- padding: `${NumCast(this.layoutDoc._yMargin, 0)}px ${NumCast(this.layoutDoc._xMargin, 0)}px`,
+ padding: `${NumCast(this.layoutDoc._yMargin, this.props.yMargin || 0)}px ${NumCast(this.layoutDoc._xMargin, this.props.xMargin || 0)}px`,
pointerEvents: ((this.layoutDoc.isLinkButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined
}} />
</div>
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index 2dd63ec21..2dd63ec21 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 7d441a48b..9ad5aafb8 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,20 +2,20 @@ import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, DocCastAsync } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { DocumentManager } from "../../util/DocumentManager";
-import { schema } from "../../util/RichTextSchema";
-import { Transform } from "../../util/Transform";
-import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import { Doc, DocCastAsync } from "../../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath, returnZero, returnOne } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { schema } from "./schema_rts";
+import { Transform } from "../../../util/Transform";
+import { ContentFittingDocumentView } from "../ContentFittingDocumentView";
import { FormattedTextBox } from "./FormattedTextBox";
import './FormattedTextBoxComment.scss';
import React = require("react");
-import { Docs } from "../../documents/Documents";
+import { Docs } from "../../../documents/Documents";
import wiki from "wikijs";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { DocumentType } from "../../../documents/DocumentTypes";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -192,18 +192,24 @@ export class FormattedTextBoxComment {
fitToBox={true}
moveDocument={returnFalse}
rootSelected={returnFalse}
- getTransform={Transform.Identity}
- active={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ parentActive={returnFalse}
addDocument={returnFalse}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
dontRegisterView={true}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
renderDepth={1}
PanelWidth={() => Math.min(350, NumCast(target._width, 350))}
PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
focus={emptyFunction}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
/>, FormattedTextBoxComment.tooltipText);
FormattedTextBoxComment.tooltip.style.width = NumCast(target.width) ? `${NumCast(target.width)}` : "100%";
FormattedTextBoxComment.tooltip.style.height = NumCast(target.height) ? `${NumCast(target.height)}` : "100%";
diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx
new file mode 100644
index 000000000..8f98da0fd
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ImageResizeView.tsx
@@ -0,0 +1,138 @@
+import { NodeSelection } from "prosemirror-state";
+import { Doc } from "../../../../new_fields/Doc";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IImageResizeView {
+ node: any;
+ view: any;
+ getPos: any;
+ addDocTab: any;
+}
+
+export class ImageResizeView extends React.Component<IImageResizeView> {
+ constructor(props: IImageResizeView) {
+ super(props);
+ }
+
+ onClickImg = (e: any) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this.props.view.state.selection.node && this.props.view.state.selection.node.type !== this.props.view.state.schema.nodes.image) {
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.view.state.selection.from - 2))));
+ }
+ }
+
+ onPointerDownImg = (e: any) => {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, this.props.view.state.schema.Document,
+ document => this.props.addDocTab(document, this.props.node.attrs.location ? this.props.node.attrs.location : "inTab"), false));
+ }
+ }
+
+ onPointerDownHandle = (e: any) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const wid = Number(getComputedStyle(elementImage).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(elementImage).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(this.props.node.attrs.width);
+
+ const onpointermove = (e: any) => {
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ elementOuter.style.width = `${startWidth + diffInPx}`;
+ elementOuter.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = this.props.view.state.selection.from;
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+ this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, width: elementOuter.style.width, height: elementOuter.style.height }));
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ }
+
+ selectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.add("ProseMirror-selectednode");
+ elementHandle.style.display = "";
+ }
+
+ deselectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.remove("ProseMirror-selectednode");
+ elementHandle.style.display = "none";
+ }
+
+
+ render() {
+
+ const outerStyle = {
+ width: this.props.node.attrs.width,
+ height: this.props.node.attrs.height,
+ display: "inline-block",
+ overflow: "hidden",
+ float: this.props.node.attrs.float
+ };
+
+ const imageStyle = {
+ width: "100%",
+ };
+
+ const handleStyle = {
+ position: "absolute",
+ width: "20px",
+ heiht: "20px",
+ backgroundColor: "blue",
+ borderRadius: "15px",
+ display: "none",
+ bottom: "-10px",
+ right: "-10px"
+
+ };
+
+
+
+ return (
+ <div id="outer"
+ style={outerStyle}
+ >
+ <img
+ id="imageId"
+ style={imageStyle}
+ src={this.props.node.src}
+ onClick={this.onClickImg}
+ onPointerDown={this.onPointerDownImg}
+
+ >
+ </img>
+ <span
+ id="handleId"
+ onPointerDown={this.onPointerDownHandle}
+ >
+
+ </span>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
index 0a3b68217..d80e64634 100644
--- a/src/client/util/ParagraphNodeSpec.ts
+++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
@@ -1,6 +1,6 @@
-import clamp from './clamp';
-import convertToCSSPTValue from './convertToCSSPTValue';
-import toCSSLineSpacing from './toCSSLineSpacing';
+import clamp from '../../../util/clamp';
+import convertToCSSPTValue from '../../../util/convertToCSSPTValue';
+import toCSSLineSpacing from '../../../util/toCSSLineSpacing';
import { Node, DOMOutputSpec } from 'prosemirror-model';
//import type { NodeSpec } from './Types';
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 356f20ce6..a0b02880e 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -5,12 +5,12 @@ 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 { SelectionManager } from "./SelectionManager";
-import { Docs } from "../documents/Documents";
-import { NumCast, BoolCast, Cast, StrCast } from "../../new_fields/Types";
-import { Doc } from "../../new_fields/Doc";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { Id } from "../../new_fields/FieldSymbols";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Docs } from "../../../documents/Documents";
+import { NumCast, BoolCast, Cast, StrCast } from "../../../../new_fields/Types";
+import { Doc } from "../../../../new_fields/Doc";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Id } from "../../../../new_fields/FieldSymbols";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
diff --git a/src/client/util/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
index 43cc23ecd..7a0718c16 100644
--- a/src/client/util/RichTextMenu.scss
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -1,4 +1,4 @@
-@import "../views/globalCssVariables";
+@import "../../globalCssVariables";
.button-dropdown-wrapper {
position: relative;
@@ -54,6 +54,29 @@
input {
color: black;
}
+
+}
+
+.richTextMenu {
+ 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;
+ }
+ }
}
.link-menu {
@@ -91,26 +114,6 @@
}
}
-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;
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 4a9a4c10f..cc04e0d6d 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,26 +1,26 @@
import React = require("react");
-import AntimodeMenu from "../views/AntimodeMenu";
+import AntimodeMenu from "../../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 { schema } from "./schema_rts";
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, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
import { updateBullets } from "./ProsemirrorExampleTransfer";
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { Cast, StrCast } from "../../new_fields/Types";
-import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
-import { unimplementedFunction, Utils } from "../../Utils";
+import { FieldViewProps } from "../FieldView";
+import { Cast, StrCast } from "../../../../new_fields/Types";
+import { FormattedTextBoxProps } from "./FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../../../Utils";
import { wrapInList } from "prosemirror-schema-list";
-import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../new_fields/SchemaHeaderField';
+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";
+import { DocServer } from "../../../DocServer";
+import { Doc } from "../../../../new_fields/Doc";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { LinkManager } from "../../../util/LinkManager";
const { toggleMark, setBlockType } = require("prosemirror-commands");
library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
diff --git a/src/client/util/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 8b11be6fb..d619bc4a0 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,16 +1,16 @@
import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
import { NodeSelection, TextSelection } from "prosemirror-state";
-import { DataSym, Doc } from "../../new_fields/Doc";
-import { Id } from "../../new_fields/FieldSymbols";
-import { ComputedField } from "../../new_fields/ScriptField";
-import { Cast, NumCast } from "../../new_fields/Types";
-import { returnFalse, Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
-import { Docs, DocUtils } from "../documents/Documents";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { DataSym, Doc } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { returnFalse, Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs, DocUtils } from "../../../documents/Documents";
+import { FormattedTextBox } from "./FormattedTextBox";
import { wrappingInputRule } from "./prosemirrorPatches";
import RichTextMenu from "./RichTextMenu";
-import { schema } from "./RichTextSchema";
+import { schema } from "./schema_rts";
export class RichTextRules {
public Document: Doc;
@@ -92,7 +92,7 @@ export class RichTextRules {
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc
new InputRule(
- new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? \-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/),
+ new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/),
(state, match, start, end) => {
const fieldKey = match[1];
const docid = match[3]?.substring(1);
@@ -110,7 +110,7 @@ export class RichTextRules {
return state.tr;
}
if (value !== "" && value !== undefined) {
- const num = value.match(/^[0-9.]/);
+ const num = value.match(/^[0-9.]$/);
this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value);
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
@@ -118,7 +118,7 @@ export class RichTextRules {
}),
// create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc
new InputRule(
- new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
(state, match, start, end) => {
const fieldKey = match[1] || "";
const fieldParam = match[2]?.replace("…", "...") || "";
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
new file mode 100644
index 000000000..cdb7374f8
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -0,0 +1,537 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+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
+ }
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any, addDocTab: any) {
+ //moved
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+ (this._outer.style as any).float = node.attrs.float;
+ //moved
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ const self = this;
+ //moved
+ this._img.onclick = function (e: any) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+ }
+ };
+ //moved
+ this._img.onpointerdown = function (e: any) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
+ document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false));
+ }
+ };
+ //moved
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = view.state.selection.from;
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
+ //Moved
+ this._outer.appendChild(this._img);
+ this._outer.appendChild(this._handle);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
+
+export class DashDocCommentView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ //moved
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = "formattedTextBox-inlineComment";
+ this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
+ this._view = view;
+ //moved
+ const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = getPos() + 1; i < view.state.doc.content.size; i++) {
+ const m = view.state.doc.nodeAt(i);
+ if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" });
+ view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc));
+ setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ };
+ //moved
+ this._collapsed.onpointerdown = (e: any) => {
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerup = (e: any) => {
+ const target = targetNode();
+ 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
+ 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) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerenter = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerleave = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ (this as any).dom = this._collapsed;
+ }
+ //moved
+ selectNode() { }
+}
+
+export class DashDocView {
+ _dashSpan: HTMLDivElement;
+ _outer: HTMLElement;
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+
+ getDocTransform = () => {
+ 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![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;
+ this._dashSpan = document.createElement("div");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.textIndent = "0";
+ this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
+ // this._outer.style.overflow = "hidden"; // bcz: not sure if this is needed. if it's used, then the doc doesn't highlight when you hover over a docComment
+ (this._outer.style as any).float = node.attrs.float;
+
+ this._dashSpan.style.width = node.attrs.width;
+ this._dashSpan.style.height = node.attrs.height;
+ this._dashSpan.style.position = "absolute";
+ this._dashSpan.style.display = "inline-block";
+ this._dashSpan.onpointerleave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ };
+ this._dashSpan.onpointerenter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ };
+ 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.Document[Id];// 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.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+ const self = this;
+ this._dashSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ };
+ this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
+ this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ 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;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+
+ if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ else {
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => {
+ this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px";
+ this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px";
+ this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: true });
+ const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ 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);
+ }
+ }
+ };
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+ // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here?
+ // }
+ 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[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ },
+ (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
+ { fireImmediately: true });
+ }
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ this._reactionDisposer?.();
+ }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ this.node = node;
+ this.outerView = view;
+ this.getPos = getPos;
+
+ // The node's representation in the editor (empty, for now)
+ this.dom = document.createElement("footnote");
+ this.dom.addEventListener("pointerup", this.toggle, true);
+ // These are used when the footnote is selected
+ this.innerView = null;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ this.innerView && this.innerView.destroy();
+ this.innerView = null;
+ this.dom.textContent = "";
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.innerView.state.applyTransaction(tr);
+ this.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.node)) return false;
+ this.node = node;
+ if (this.innerView) {
+ const state = this.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+}
+
+export class SummaryView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = this.className(node.attrs.visibility);
+ this._view = view;
+ const js = node.toJSON;
+ node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ this._collapsed.onpointerdown = (e: any) => {
+ const visible = !node.attrs.visibility;
+ const attrs = { ...node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ 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
+ e.preventDefault();
+ e.stopPropagation();
+ this._collapsed.className = this.className(visible);
+ };
+ (this as any).dom = this._collapsed;
+ }
+ selectNode() { }
+
+ deselectNode() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ updateSummarizedText(start?: any) {
+ const mtype = this._view.state.schema.marks.summarize;
+ const mtypeInc = this._view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (node.isLeaf && !visited.has(node) && !skip) {
+ if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this._view.state.doc, start, endPos);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
new file mode 100644
index 000000000..89908d8ee
--- /dev/null
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -0,0 +1,81 @@
+import { TextSelection } from "prosemirror-state";
+import { Fragment, Node, Slice } from "prosemirror-model";
+
+import React = require("react");
+
+interface ISummaryView {
+ node: any;
+ view: any;
+ getPos: any;
+ self: any;
+}
+export class SummaryView extends React.Component<ISummaryView> {
+
+ onPointerDown = (e: any) => {
+ const visible = !this.props.node.attrs.visibility;
+ const attrs = { ...this.props.node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(this.props.view.state.doc, this.props.getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(this.props.getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ this.props.view.dispatch(this.props.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) : this.props.node.attrs.text). // collapse/expand it
+ setNodeMarkup(this.props.getPos(), undefined, attrs)); // update the attrs
+ e.preventDefault();
+ e.stopPropagation();
+ const _collapsed = document.getElementById('collapse') as HTMLElement;
+ _collapsed.className = this.className(visible);
+ }
+
+ updateSummarizedText(start?: any) {
+ const mtype = this.props.view.state.schema.marks.summarize;
+ const mtypeInc = this.props.view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this.props.view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this.props.view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (this.props.node.isLeaf && !visited.has(node) && !skip) {
+ if (this.props.node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + this.props.node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this.props.view.state.doc, start, endPos);
+ }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ selectNode() { }
+
+ deselectNode() { }
+
+ render() {
+ const _view = this.props.node.view;
+ const js = this.props.node.toJSon;
+
+ this.props.node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ const spanCollapsedClassName = this.className(this.props.node.attrs.visibility);
+
+ return (
+ <span
+ className={spanCollapsedClassName}
+ id='collapse'
+ onPointerDown={this.onPointerDown}
+ >
+
+ </span>
+ );
+
+ }
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
index e2149e9c1..e2149e9c1 100644
--- a/src/client/util/TooltipTextMenu.scss
+++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
new file mode 100644
index 000000000..46bf481fb
--- /dev/null
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -0,0 +1,296 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { Doc } from "../../../../new_fields/Doc";
+
+
+const emDOM: DOMOutputSpecArray = ["em", 0];
+const strongDOM: DOMOutputSpecArray = ["strong", 0];
+const codeDOM: DOMOutputSpecArray = ["code", 0];
+
+// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
+export const marks: { [index: string]: MarkSpec } = {
+ // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // defaults to the empty string. Rendered and parsed as an `<a>`
+ // element.
+ link: {
+ attrs: {
+ href: {},
+ targetId: { default: "" },
+ linkId: { default: "" },
+ showPreview: { default: true },
+ location: { default: null },
+ title: { default: null },
+ docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.docref && node.attrs.title ?
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
+ pFontColor: {
+ attrs: {
+ color: { default: "#000" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { color: dom.getAttribute("color") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0];
+ }
+ },
+
+ marker: {
+ attrs: {
+ highlight: { default: "transparent" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { highlight: dom.getAttribute("backgroundColor") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
+ }
+ },
+
+ // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
+ // Has parse rules that also match `<i>` and `font-style: italic`.
+ em: {
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
+ toDOM() { return emDOM; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
+ // also match `<b>` and `font-weight: bold`.
+ strong: {
+ parseDOM: [{ tag: "strong" },
+ { tag: "b" },
+ { style: "font-weight" }],
+ toDOM() { return strongDOM; }
+ },
+
+ strikethrough: {
+ parseDOM: [
+ { tag: 'strike' },
+ { style: 'text-decoration=line-through' },
+ { style: 'text-decoration-line=line-through' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration-line:line-through'
+ }]
+ },
+
+ subscript: {
+ excludes: 'superscript',
+ parseDOM: [
+ { tag: 'sub' },
+ { style: 'vertical-align=sub' }
+ ],
+ toDOM: () => ['sub']
+ },
+
+ superscript: {
+ excludes: 'subscript',
+ parseDOM: [
+ { tag: 'sup' },
+ { style: 'vertical-align=super' }
+ ],
+ toDOM: () => ['sup']
+ },
+
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
+ metadata: {
+ toDOM() {
+ return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+ }
+ },
+ metadataKey: {
+ toDOM() {
+ return ['span', { style: 'font-style:italic; ' }];
+ }
+ },
+ metadataVal: {
+ toDOM() {
+ return ['span'];
+ }
+ },
+
+ summarizeInclusive: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ inclusive: true,
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ summarize: {
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ underline: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ }
+ // { style: "text-decoration=underline" }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline;text-decoration-style:line'
+ }]
+ },
+
+ search_highlight: {
+ attrs: {
+ selected: { default: false }
+ },
+ parseDOM: [{ style: 'background: yellow' }],
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
+ }];
+ }
+ },
+
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ },
+ group: "inline",
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ const min = Math.round(node.attrs.modified / 12);
+ const hr = Math.round(min / 60);
+ const day = Math.round(hr / 60 / 24);
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
+ }
+ },
+ // the id of the user who entered the text
+ user_tag: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ tag: { default: "" }
+ },
+ group: "inline",
+ inclusive: false,
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Code font mark. Represented as a `<code>` element.
+ code: {
+ parseDOM: [{ tag: "code" }],
+ toDOM() { return codeDOM; }
+ },
+
+ /* FONTS */
+ pFontFamily: {
+ attrs: {
+ family: { default: "Crimson Text" },
+ },
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ const cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+ if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+ if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+ if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+ if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+ if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+ }
+ }
+ }],
+ toDOM: (node) => ['span', {
+ style: `font-family: "${node.attrs.family}";`
+ }]
+ },
+
+ /** FONT SIZES */
+ pFontSize: {
+ attrs: {
+ fontSize: { default: 10 }
+ },
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: (node) => ['span', {
+ style: `font-size: ${node.attrs.fontSize}px;`
+ }]
+ },
+};
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
new file mode 100644
index 000000000..e7bcf444a
--- /dev/null
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -0,0 +1,264 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
+
+const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
+ preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+
+// :: Object
+// [Specs](#model.NodeSpec) for the nodes defined in this schema.
+export const nodes: { [index: string]: NodeSpec } = {
+ // :: NodeSpec The top level document node.
+ doc: {
+ content: "block+"
+ },
+
+ footnote: {
+ group: "inline",
+ content: "inline*",
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ parseDOM: [{ tag: "footnote" }]
+ },
+
+ paragraph: ParagraphNodeSpec,
+
+ // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`<hr>`).
+ horizontal_rule: {
+ group: "block",
+ parseDOM: [{ tag: "hr" }],
+ toDOM() { return hrDOM; }
+ },
+
+ // :: NodeSpec A heading textblock, with a `level` attribute that
+ // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
+ // `<h6>` elements.
+ heading: {
+ attrs: { level: { default: 1 } },
+ content: "inline*",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "h1", attrs: { level: 1 } },
+ { tag: "h2", attrs: { level: 2 } },
+ { tag: "h3", attrs: { level: 3 } },
+ { tag: "h4", attrs: { level: 4 } },
+ { tag: "h5", attrs: { level: 5 } },
+ { tag: "h6", attrs: { level: 6 } }],
+ toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ },
+
+ // :: NodeSpec A code listing. Disallows marks or non-text inline
+ // nodes by default. Represented as a `<pre>` element with a
+ // `<code>` element inside of it.
+ code_block: {
+ content: "text*",
+ marks: "",
+ group: "block",
+ code: true,
+ defining: true,
+ parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
+ toDOM() { return preDOM; }
+ },
+
+ // :: NodeSpec The text node.
+ text: {
+ group: "inline"
+ },
+
+ dashComment: {
+ attrs: {
+ docid: { default: "" },
+ },
+ inline: true,
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }, "←"];
+ },
+ },
+
+ summary: {
+ inline: true,
+ attrs: {
+ visibility: { default: false },
+ text: { default: undefined },
+ textslice: { default: undefined },
+ },
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }];
+ },
+ },
+
+ // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
+ // `alt`, and `href` attributes. The latter two default to the empty
+ // string.
+ image: {
+ inline: true,
+ attrs: {
+ src: {},
+ agnostic: { default: null },
+ width: { default: 100 },
+ alt: { default: null },
+ title: { default: null },
+ float: { default: "left" },
+ location: { default: "onRight" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "img[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ // TODO if we don't define toDom, dragging the image crashes. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashDoc: {
+ inline: true,
+ attrs: {
+ width: { default: 200 },
+ height: { default: 100 },
+ title: { default: null },
+ 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,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ video: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "video[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["video", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
+ hard_break: {
+ inline: true,
+ group: "inline",
+ selectable: false,
+ parseDOM: [{ tag: "br" }],
+ toDOM() { return brDOM; }
+ },
+
+ ordered_list: {
+ ...orderedList,
+ content: 'list_item+',
+ group: 'block',
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ setFontSize: { default: undefined },
+ setFontFamily: { default: "inherit" },
+ setFontColor: { default: "inherit" },
+ inheritedFontSize: { default: undefined },
+ visibility: { default: true },
+ indent: { default: undefined }
+ },
+ toDOM(node: Node<any>) {
+ if (node.attrs.mapStyle === "bullet") return ['ul', 0];
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+ const ffam = node.attrs.setFontFamily;
+ const color = node.attrs.setFontColor;
+ return node.attrs.visibility ?
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ }
+ },
+
+ bullet_list: {
+ ...bulletList,
+ content: 'list_item+',
+ group: 'block',
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ toDOM(node: Node<any>) {
+ return ['ul', 0];
+ }
+ },
+
+ list_item: {
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
+ ...listItem,
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+ //return ["li", { class: `${map}` }, 0];
+ }
+ },
+}; \ No newline at end of file
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js
index 269423482..269423482 100644
--- a/src/client/util/prosemirrorPatches.js
+++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js
diff --git a/src/client/views/nodes/formattedText/schema_rts.ts b/src/client/views/nodes/formattedText/schema_rts.ts
new file mode 100644
index 000000000..83561073c
--- /dev/null
+++ b/src/client/views/nodes/formattedText/schema_rts.ts
@@ -0,0 +1,26 @@
+import { Schema, Slice } from "prosemirror-model";
+
+import { nodes } from "./nodes_rts";
+import { marks } from "./marks_rts";
+
+
+// :: Schema
+// This schema rougly corresponds to the document schema used by
+// [CommonMark](http://commonmark.org/), minus the list elements,
+// which are defined in the [`prosemirror-schema-list`](#schema-list)
+// module.
+//
+// To reuse elements from this schema, extend or read from its
+// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
+
+export const schema = new Schema({ nodes, marks });
+
+const fromJson = schema.nodeFromJSON;
+
+schema.nodeFromJSON = (json: any) => {
+ const node = fromJson(json);
+ if (json.type === schema.nodes.summary.name) {
+ node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
+ }
+ return node;
+}; \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 760f64a72..8541a3149 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -56,10 +56,6 @@
left: 0px;
display: inline-block;
width:100%;
- pointer-events: none;
- .collectionFreeFormDocumentView-container {
- pointer-events: all;
- }
}
.pdfViewer-annotationLayer {
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index 8370af490..ccd2e8947 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -1,5 +1,4 @@
.presElementBox-item {
- padding: 10px;
display: inline-block;
background-color: #eeeeee;
pointer-events: all;
@@ -16,7 +15,6 @@
user-select: none;
transition: all .1s;
padding: 0px;
- padding-left: 5px;
padding-bottom: 3px;
.documentView-node {
position: absolute;
@@ -38,7 +36,7 @@
border-radius: 6px;
}
-.presElementBox-selected {
+.presElementBox-active {
background: gray;
color: black;
border-radius: 6px;
@@ -54,20 +52,27 @@
padding: 8px;
}
-.presElementBox-interaction {
- color: gray;
- float: left;
- padding: 0px;
- width: 20px;
- height: 20px;
-}
-.presElementBox-interaction-selected {
- color: white;
- float: left;
- padding: 0px;
- width: 22px;
- height: 22px;
+.presElementBox-buttons {
+ display: flow-root;
+ position: relative;
+ width: 100%;
+ height: auto;
+ .presElementBox-interaction {
+ color: gray;
+ float: left;
+ padding: 0px;
+ width: 20px;
+ height: 20px;
+ }
+ .presElementBox-interaction-selected {
+ color: white;
+ float: left;
+ padding: 0px;
+ width: 20px;
+ height: 20px;
+ border: solid 1px darkgray;
+ }
}
.presElementBox-name {
@@ -82,14 +87,10 @@
.presElementBox-embedded {
position: relative;
- margin-top: 22;
display: flex;
width: auto;
justify-content: center;
- .contentFittingDocumentView {
- position: absolute;
- height: 100%;
- }
+ margin:auto;
}
.presElementBox-embeddedMask {
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index dd0cbf929..6d7602268 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -1,15 +1,12 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
-import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DataSym } from "../../../new_fields/Doc";
+import { Doc, DataSym, DocListCast } from "../../../new_fields/Doc";
import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id } from "../../../new_fields/FieldSymbols";
import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { Cast, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, emptyPath, returnFalse, returnTrue } from "../../../Utils";
+import { Cast, NumCast, BoolCast, ScriptCast } from "../../../new_fields/Types";
+import { emptyFunction, emptyPath, returnFalse, returnTrue, returnOne, returnZero } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { CollectionViewType } from '../collections/CollectionView';
import { ViewBoxBaseComponent } from '../DocComponent';
@@ -18,23 +15,16 @@ import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./PresElementBox.scss";
import React = require("react");
-library.add(faArrowUp);
-library.add(fileSolid);
-library.add(faLocationArrow);
-library.add(fileRegular as any);
-library.add(faSearch);
-library.add(faArrowDown);
-
export const presSchema = createSchema({
presentationTargetDoc: Doc,
presBox: Doc,
- zoomButton: "boolean",
- navButton: "boolean",
- hideTillShownButton: "boolean",
- fadeButton: "boolean",
- hideAfterButton: "boolean",
- groupButton: "boolean",
- expandInlineButton: "boolean"
+ presZoomButton: "boolean",
+ presNavButton: "boolean",
+ presHideTillShownButton: "boolean",
+ presFadeButton: "boolean",
+ presHideAfterButton: "boolean",
+ presGroupButton: "boolean",
+ presExpandInlineButton: "boolean"
});
type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
@@ -48,16 +38,16 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
_heightDisposer: IReactionDisposer | undefined;
- @computed get indexInPres() { return NumCast(this.presElementDoc?.presentationIndex); }
- @computed get presBoxDoc() { return Cast(this.presElementDoc?.presBox, Doc) as Doc; }
- @computed get presElementDoc() { return this.rootDoc; }
- @computed get presLayoutDoc() { return this.layoutDoc; }
- @computed get targetDoc() { return this.presElementDoc?.presentationTargetDoc as Doc; }
- @computed get currentIndex() { return NumCast(this.presBoxDoc?._itemIndex); }
+ // these fields are conditionally computed fields on the layout document that take this document as a parameter
+ @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+ @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation elemnt template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+ @computed get presStatus() { return BoolCast(this.layoutDoc.presStatus); }
+ @computed get currentIndex() { return NumCast(this.layoutDoc.currentIndex); }
+ @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
componentDidMount() {
- this._heightDisposer = reaction(() => [this.presElementDoc.expandInlineButton, this.presElementDoc.collapsedHeight],
- params => this.presLayoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+ params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
}
componentWillUnmount() {
this._heightDisposer?.();
@@ -70,13 +60,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.hideTillShownButton = !this.presElementDoc.hideTillShownButton;
- if (!this.presElementDoc.hideTillShownButton) {
+ this.rootDoc.presHideTillShownButton = !this.rootDoc.presHideTillShownButton;
+ if (!this.rootDoc.presHideTillShownButton) {
if (this.indexInPres >= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.presBoxDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+ if (this.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -90,14 +80,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.hideAfterButton = !this.presElementDoc.hideAfterButton;
- if (!this.presElementDoc.hideAfterButton) {
+ this.rootDoc.presHideAfterButton = !this.rootDoc.presHideAfterButton;
+ if (!this.rootDoc.presHideAfterButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.presElementDoc.fadeButton) this.presElementDoc.fadeButton = false;
- if (this.presBoxDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+ if (this.rootDoc.presFadeButton) this.rootDoc.presFadeButton = false;
+ if (this.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -111,14 +101,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.fadeButton = !this.presElementDoc.fadeButton;
- if (!this.presElementDoc.fadeButton) {
+ this.rootDoc.presFadeButton = !this.rootDoc.presFadeButton;
+ if (!this.rootDoc.presFadeButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- this.presElementDoc.hideAfterButton = false;
- if (this.presBoxDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+ this.rootDoc.presHideAfterButton = false;
+ if (this.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
this.targetDoc.opacity = 0.5;
}
}
@@ -130,11 +120,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
@action
onNavigateDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.navButton = !this.presElementDoc.navButton;
- if (this.presElementDoc.navButton) {
- this.presElementDoc.zoomButton = false;
+ this.rootDoc.presNavButton = !this.rootDoc.presNavButton;
+ if (this.rootDoc.presNavButton) {
+ this.rootDoc.presZoomButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.presElementDoc);
+ this.props.focus(this.rootDoc);
}
}
}
@@ -146,13 +136,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
onZoomDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.presElementDoc.zoomButton = !this.presElementDoc.zoomButton;
- if (!this.presElementDoc.zoomButton) {
- this.presElementDoc.viewScale = 1;
- } else {
- this.presElementDoc.navButton = false;
+ this.rootDoc.presZoomButton = !this.rootDoc.presZoomButton;
+ if (this.rootDoc.presZoomButton) {
+ this.rootDoc.presNavButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.presElementDoc);
+ this.props.focus(this.rootDoc);
}
}
}
@@ -161,18 +149,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
*/
ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
- embedHeight = () => this.props.PanelHeight() - NumCast(this.presElementDoc.collapsedHeight);
+ embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
embedWidth = () => this.props.PanelWidth() - 20;
/**
* The function that is responsible for rendering the a preview or not for this
* presentation element.
*/
- renderEmbeddedInline = () => {
- return !this.presElementDoc.expandInlineButton || !this.targetDoc ? (null) :
- <div className="presElementBox-embedded" style={{ height: this.embedHeight() }}>
+ @computed get renderEmbeddedInline() {
+ return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+ <div className="presElementBox-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
<ContentFittingDocumentView
Document={this.targetDoc}
- DataDocument={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+ DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
LibraryPath={emptyPath}
fitToBox={true}
rootSelected={returnTrue}
@@ -182,12 +170,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
pinToPres={returnFalse}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
- getTransform={Transform.Identity}
- active={this.props.active}
+ ScreenToLocalTransform={Transform.Identity}
+ parentActive={this.props.active}
moveDocument={this.props.moveDocument!}
renderDepth={this.props.renderDepth + 1}
focus={emptyFunction}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={returnOne}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
/>
<div className="presElementBox-embeddedMask" />
</div>;
@@ -195,29 +189,29 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
render() {
const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
- const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
+ const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-active" : "");
const pbi = "presElementBox-interaction";
- return !(this.presElementDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : (
+ return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : (
<div className={className} key={this.props.Document[Id] + this.indexInPres}
style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
- onClick={e => { this.props.focus(this.presElementDoc); e.stopPropagation(); }}>
+ onClick={e => { this.props.focus(this.rootDoc); e.stopPropagation(); }}>
{treecontainer ? (null) : <>
<strong className="presElementBox-name">
{`${this.indexInPres + 1}. ${this.targetDoc?.title}`}
</strong>
- <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument?.(this.presElementDoc)}>X</button>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument?.(this.rootDoc)}>X</button>
<br />
</>}
- <button title="Zoom" className={pbi + (this.presElementDoc.zoomButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={pbi + (this.presElementDoc.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Before" className={pbi + (this.presElementDoc.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade After" className={pbi + (this.presElementDoc.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Hide After" className={pbi + (this.presElementDoc.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={pbi + (this.presElementDoc.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.groupButton = !this.presElementDoc.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
- <button title="Expand Inline" className={pbi + (this.presElementDoc.expandInlineButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.expandInlineButton = !this.presElementDoc.expandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
-
- <br style={{ lineHeight: 0.1 }} />
- {this.renderEmbeddedInline()}
+ <div className="presElementBox-buttons">
+ <button title="Zoom" className={pbi + (this.rootDoc.presZoomButton ? "-selected" : "")} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Navigate" className={pbi + (this.rootDoc.presNavButton ? "-selected" : "")} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Hide Before" className={pbi + (this.rootDoc.presHideTillShownButton ? "-selected" : "")} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={"file"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Fade After" className={pbi + (this.rootDoc.presFadeButton ? "-selected" : "")} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Hide After" className={pbi + (this.rootDoc.presHideAfterButton ? "-selected" : "")} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={"file-download"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Group With Up" className={pbi + (this.rootDoc.presGroupButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presGroupButton = !this.rootDoc.presGroupButton; }}><FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} /></button>
+ <button title="Expand Inline" className={pbi + (this.rootDoc.presExpandInlineButton ? "-selected" : "")} onClick={e => { e.stopPropagation(); this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} onPointerDown={e => e.stopPropagation()} /></button>
+ </div>
+ {this.renderEmbeddedInline}
</div>
);
}
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index fe2000700..96f43e931 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue } from "../../../Utils";
+import { emptyFunction, emptyPath, returnFalse, Utils, returnTrue, returnOne, returnZero } from "../../../Utils";
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -164,14 +164,20 @@ export class SearchItem extends React.Component<SearchItemProps> {
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
- getTransform={Transform.Identity}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ ScreenToLocalTransform={Transform.Identity}
renderDepth={1}
PanelWidth={returnXDimension}
PanelHeight={returnYDimension}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
focus={emptyFunction}
moveDocument={returnFalse}
- active={returnFalse}
+ parentActive={returnFalse}
whenActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ ContentScaling={returnOne}
/>
</div>;
return docview;
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 73ebbb303..69a80e1b4 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -7,7 +7,7 @@ import { observer } from 'mobx-react';
import { DocServer } from '../client/DocServer';
import { Docs } from '../client/documents/Documents';
import { DocumentManager } from '../client/util/DocumentManager';
-import RichTextMenu from '../client/util/RichTextMenu';
+import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu';
import { Scripting } from '../client/util/Scripting';
import { Transform } from '../client/util/Transform';
import { CollectionView } from '../client/views/collections/CollectionView';
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 337274774..9efb14d03 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -183,7 +183,7 @@ export class Doc extends RefField {
let renderFieldKey: any;
const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, "layout")];
if (typeof layoutField === "string") {
- renderFieldKey = layoutField.split("'")[1];
+ renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0];//layoutField.split("'")[1];
} else {
return Cast(layoutField, Doc, null);
}
@@ -254,6 +254,7 @@ export namespace Doc {
// return Cast(field, ctor);
// });
// }
+
export function RunCachedUpdate(doc: Doc, field: string) {
const update = doc[CachedUpdates][field];
if (update) {
@@ -291,6 +292,9 @@ export namespace Doc {
export function IsPrototype(doc: Doc) {
return GetT(doc, "isPrototype", "boolean", true);
}
+ export function IsBaseProto(doc: Doc) {
+ return GetT(doc, "baseProto", "boolean", true);
+ }
export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
const hasProto = doc.proto instanceof Doc;
const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
@@ -580,30 +584,44 @@ export namespace Doc {
return copy;
}
- export function MakeClone(doc: Doc, cloneProto: boolean = true): Doc {
+ export function MakeClone(doc: Doc, cloneMap: Map<Doc, Doc> = new Map()): Doc {
+ if (Doc.IsBaseProto(doc)) return doc;
+ if (cloneMap.get(doc)) return cloneMap.get(doc)!;
const copy = new Doc(undefined, true);
+ cloneMap.set(doc, copy);
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" && cloneProto) {
+ if (key === "proto") {
if (doc[key] instanceof Doc) {
- copy[key] = Doc.MakeClone(doc[key]!, false);
+ copy[key] = Doc.MakeClone(doc[key]!, cloneMap);
}
} else {
if (field instanceof RefField) {
copy[key] = field;
} else if (cfield instanceof ComputedField) {
copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
+ if (field instanceof ObjectField) {
+ const list = Cast(doc[key], listSpec(Doc));
+ if (list !== undefined && !(list instanceof Promise)) {
+ copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeClone(d as Doc, cloneMap)));
+ } else {
+ copy[key] = doc[key] instanceof Doc ?
+ key.includes("layout[") ?
+ undefined : Doc.MakeClone(doc[key] as Doc, cloneMap) : // reference documents except copy documents that are expanded teplate fields
+ ObjectField.MakeCopy(field);
+ }
+ }
} else if (field instanceof ObjectField) {
const list = Cast(doc[key], listSpec(Doc));
if (list !== undefined && !(list instanceof Promise)) {
- copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeCopy(d as Doc, false)));
+ copy[key] = new List<Doc>(list.filter(d => d instanceof Doc).map(d => Doc.MakeClone(d as Doc, cloneMap)));
} else {
copy[key] = doc[key] instanceof Doc ?
key.includes("layout[") ?
- Doc.MakeCopy(doc[key] as Doc, false) : doc[key] : // reference documents except copy documents that are expanded teplate fields
+ undefined : Doc.MakeClone(doc[key] as Doc, cloneMap) : // reference documents except copy documents that are expanded teplate fields
ObjectField.MakeCopy(field);
}
} else if (field instanceof Promise) {
@@ -615,6 +633,7 @@ export namespace Doc {
});
Doc.SetInPlace(copy, "title", "CLONE: " + doc.title, true);
copy.cloneOf = doc;
+ cloneMap.set(doc, copy);
return copy;
}
@@ -771,7 +790,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) ? "layout_key1" : "layout_key2"; }
+ export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "1" : "2"; }
export function linkFollowUnhighlight() {
Doc.UnhighlightAll();
@@ -996,7 +1015,7 @@ export namespace Doc {
//newCollection.borderRounding = "40px";
newCollection._jitterRotation = 10;
newCollection._backgroundColor = "gray";
- newCollection.overflow = "visible";
+ newCollection._overflow = "visible";
return newCollection;
}
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 635fd053d..c475d0d73 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -2,20 +2,20 @@ import { AssertionError } from "assert";
import { docs_v1 } from "googleapis";
import { Fragment, Mark, Node } from "prosemirror-model";
import { sinkListItem } from "prosemirror-schema-list";
-import { EditorState, TextSelection, Transaction } from "prosemirror-state";
-import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+import { Utils } from "../Utils";
+import { Docs } from "../client/documents/Documents";
+import { schema } from "../client/views/nodes/formattedText/schema_rts";
import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
import { DocServer } from "../client/DocServer";
-import { Docs } from "../client/documents/Documents";
import { Networking } from "../client/Network";
-import { schema } from "../client/util/RichTextSchema";
-import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
-import { Utils } from "../Utils";
+import { FormattedTextBox } from "../client/views/nodes/formattedText/FormattedTextBox";
import { Doc, Opt } from "./Doc";
import { Id } from "./FieldSymbols";
import { RichTextField } from "./RichTextField";
import { Cast, StrCast } from "./Types";
import Color = require('color');
+import { EditorState, TextSelection, Transaction } from "prosemirror-state";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
export namespace RichTextUtils {
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index aa44cefa0..3d784448d 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -88,8 +88,8 @@ export function DateCast(field: FieldResult) {
return Cast(field, DateField, null);
}
-export function ScriptCast(field: FieldResult) {
- return Cast(field, ScriptField, null);
+export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) {
+ return Cast(field, ScriptField, defaultVal);
}
type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 7a0be8863..e7d27d81e 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -4,13 +4,25 @@ import { Doc } from "./Doc";
import { DateField } from "./DateField";
export const documentSchema = createSchema({
+ // content properties
type: "string", // enumerated type of document -- should be template-specific (ie, start with an '_')
- layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
- layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
title: "string", // document title (can be on either data document or layout)
- dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
- targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move")
- childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+ isTemplateForField: "string",// if specified, it indicates the document is a template that renders the specified field
+ creationDate: DateField, // when the document was created
+ links: listSpec(Doc), // computed (readonly) list of links associated with this document
+
+ // "Location" properties in a very general sense
+ currentTimecode: "number", // current play back time of a temporal document (video / audio)
+ displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+ inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
+ x: "number", // x coordinate when in a freeform view
+ y: "number", // y coordinate when in a freeform view
+ z: "number", // z "coordinate" - non-zero specifies the overlay layer of a freeformview
+ zIndex: "number", // zIndex of a document in a freeform view
+ scrollY: "number", // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
+ scrollTop: "number", // scroll position of a scrollable document (pdf, text, web)
+
+ // appearance properties on the layout
_autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
_nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
_nativeHeight: "number", // "
@@ -20,72 +32,68 @@ export const documentSchema = createSchema({
_yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitToBox is set
_xMargin: "number", // margin added on left/right of most documents to add separation from their container
_yMargin: "number", // margin added on top/bottom of most documents to add separation from their container
+ _overflow: "string", // sets overflow behvavior for CollectionFreeForm views
_showCaption: "string", // whether editable caption text is overlayed at the bottom 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
_showAudio: "boolean", // whether to show the audio record icon on documents
_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
- _pivotField: "string", // specifies which field should be used as the timeline/pivot axis
+ _pivotField: "string", // specifies which field key should be used as the timeline/pivot axis
_replacedChrome: "string", // what the default chrome is replaced with. Currently only supports the value of 'replaced' for PresBox's.
_chromeStatus: "string", // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed'
- _freezeChildDimensions: "boolean", // freezes child document dimensions (e.g., used by time/pivot view to make sure all children will be scaled to fit their display rectangle)
_fontSize: "number",
_fontFamily: "string",
- isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
- color: "string", // foreground color of document
+
+ // appearance properties on the data document
backgroundColor: "string", // background color of document
+ borderRounding: "string", // border radius rounding of document
+ boxShadow: "string", // the amount of shadow around the perimeter of a document
+ color: "string", // foreground color of document
+ fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
+ fontSize: "string",
+ layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
+ layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
+ letterSpacing: "string",
opacity: "number", // opacity of document
- overflow: "string", // sets overflow behvavior for CollectionFreeForm views
- creationDate: DateField, // when the document was created
- links: listSpec(Doc), // computed (readonly) list of links associated with this document
+ strokeWidth: "number",
+ textTransform: "string",
+ treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
+ treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
+ treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+
+ // interaction and linking properties
+ ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
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)
onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
- dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
- removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
- 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)
- treeViewOpen: "boolean", // flag denoting whether the documents sub-tree (contents) is visible or hidden
- treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
- treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
- currentTimecode: "number", // current play back time of a temporal document (video / audio)
followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab, )
+ isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
+ isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked
+ isBackground: "boolean", // whether document is a background element and ignores input events (can only select with marquee)
lockedPosition: "boolean", // whether the document can be moved (dragged)
lockedTransform: "boolean", // whether the document can be panned/zoomed
- inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
- borderRounding: "string", // border radius rounding of document
- heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
- isLinkButton: "boolean", // whether document functions as a link follow button to follow the first link on the document when clicked
- ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
- 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.
- scrollY: "number", // "command" to scroll a document to a position on load (the value will be reset to 0 after that )
- scrollTop: "number", // scroll position of a scrollable document (pdf, text, web)
- strokeWidth: "number",
- fontSize: "string",
- fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
- letterSpacing: "string",
- textTransform: "string",
- childTemplateName: "string" // the name of a template to use to override the layoutKey when rendering a document in DocHolderBox
-});
-export const positionSchema = createSchema({
- zIndex: "number",
- x: "number",
- y: "number",
- z: "number",
+ // drag drop properties
+ dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
+ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
+ targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
+ childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and 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
});
+
export const collectionSchema = createSchema({
- childLayout: Doc, // layout template for children of a collecion
- childDetailView: 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)
+ childLayoutTemplateName: "string", // the name of a template to use to override the layoutKey when rendering a document -- ONLY used in DocHolderBox
+ childLayoutTemplate: Doc, // layout template to use to render children of a collecion
+ childLayoutString: "string", //layout string to use to render children of a collection
+ childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
+ dontRegisterChildViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links 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.
onChildClick: ScriptField, // script to run for each child when its clicked
+ onChildDoubleClick: 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);
-
-export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-export const PositionDocument = makeInterface(documentSchema, positionSchema);
diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts
index 713207a07..94302c7b3 100644
--- a/src/scraping/buxton/final/BuxtonImporter.ts
+++ b/src/scraping/buxton/final/BuxtonImporter.ts
@@ -10,6 +10,10 @@ import { parseXml } from "libxmljs";
import { strictEqual } from "assert";
import { Readable, PassThrough } from "stream";
+/**
+ * This is an arbitrary bundle of data that gets populated
+ * in extractFileContents
+ */
interface DocumentContents {
body: string;
imageData: ImageData[];
@@ -19,6 +23,10 @@ interface DocumentContents {
longDescription: string;
}
+/**
+ * A rough schema for everything that Bill has
+ * included for each document
+ */
export interface DeviceDocument {
title: string;
shortDescription: string;
@@ -33,36 +41,65 @@ export interface DeviceDocument {
attribute: string;
__images: ImageData[];
hyperlinks: string[];
- captions: string[];
- embeddedFileNames: string[];
+ captions: string[]; // from the table column
+ embeddedFileNames: string[]; // from the table column
}
+/**
+ * A layer of abstraction around a single parsing
+ * attempt. The error is not a TypeScript error, but
+ * rather an invalidly formatted value for a given key.
+ */
export interface AnalysisResult {
device?: DeviceDocument;
- errors?: { [key: string]: string };
+ invalid?: { [deviceProperty: string]: string };
}
+/**
+ * A mini API that takes in a string and returns
+ * either the given T or an error indicating that the
+ * transformation was rejected.
+ */
type Transformer<T> = (raw: string) => TransformResult<T>;
interface TransformResult<T> {
transformed?: T;
error?: string;
}
+/**
+ * Simple bundle counting successful and failed imports
+ */
export interface ImportResults {
deviceCount: number;
errorCount: number;
}
+/**
+ * Definitions for callback functions. Such instances are
+ * just invoked by when a single document has been parsed
+ * or the entire import is over. As of this writing, these
+ * callbacks are supplied by WebSocket.ts and used to inform
+ * the client of these events.
+ */
type ResultCallback = (result: AnalysisResult) => void;
type TerminatorCallback = (result: ImportResults) => void;
-interface Processor<T> {
- exp: RegExp;
- matchIndex?: number;
- transformer?: Transformer<T>;
- required?: boolean;
+/**
+ * Defines everything needed to define how a single key should be
+ * formatted within the plain body text. The association between
+ * keys and their format definitions is stored FormatMap
+ */
+interface ValueFormatDefinition<T> {
+ exp: RegExp; // the expression that the key's value should match
+ matchIndex?: number; // defaults to 0, but can be overridden to account for grouping in @param exp
+ transformer?: Transformer<T>; // if desirable, how to transform the Regex match
+ required?: boolean; // defaults to true, confirms that for a whole document to be counted successful,
+ // all of its required values should be present and properly formatted
}
+/**
+ * The basic data we extract from each image in the document
+ */
interface ImageData {
url: string;
nativeWidth: number;
@@ -71,6 +108,10 @@ interface ImageData {
namespace Utilities {
+ /**
+ * Numeric 'try parse', fits with the Transformer API
+ * @param raw the serialized number
+ */
export function numberValue(raw: string): TransformResult<number> {
const transformed = Number(raw);
if (isNaN(transformed)) {
@@ -79,18 +120,32 @@ namespace Utilities {
return { transformed };
}
+ /**
+ * A simple tokenizer that splits along 'and' and commas, and removes duplicates
+ * Helpful mainly for attribute and primary key lists
+ * @param raw the string to tokenize
+ */
export function collectUniqueTokens(raw: string): TransformResult<string[]> {
const pieces = raw.replace(/,|\s+and\s+/g, " ").split(/\s+/).filter(piece => piece.length);
const unique = new Set(pieces.map(token => token.toLowerCase().trim()));
return { transformed: Array.from(unique).map(capitalize).sort() };
}
+ /**
+ * Tries to correct XML text parsing artifact where some sentences lose their separating space,
+ * and others gain excess whitespace
+ * @param raw
+ */
export function correctSentences(raw: string): TransformResult<string> {
raw = raw.replace(/\./g, ". ").replace(/\:/g, ": ").replace(/\,/g, ", ").replace(/\?/g, "? ").trimRight();
raw = raw.replace(/\s{2,}/g, " ");
return { transformed: raw };
}
+ /**
+ * Simple capitalization
+ * @param word to capitalize
+ */
export function capitalize(word: string): string {
const clean = word.trim();
if (!clean.length) {
@@ -99,6 +154,12 @@ namespace Utilities {
return word.charAt(0).toUpperCase() + word.slice(1);
}
+ /**
+ * Streams the requeted file at the relative path to the
+ * root of the zip, then parses it with a library
+ * @param zip the zip instance data source
+ * @param relativePath the path to a .xml file within the zip to parse
+ */
export async function readAndParseXml(zip: any, relativePath: string) {
console.log(`Text streaming ${relativePath}`);
const contents = await new Promise<string>((resolve, reject) => {
@@ -111,13 +172,17 @@ namespace Utilities {
stream.on('end', () => resolve(body));
});
});
-
return parseXml(contents);
}
-
}
-const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
+/**
+ * Defines how device values should be formatted. As you can see, the formatting is
+ * not super consistent and has changed over time as edge cases have been found, but this
+ * at least imposes some constraints, and will notify you if a document doesn't match the specifications
+ * in this map.
+ */
+const FormatMap = new Map<keyof DeviceDocument, ValueFormatDefinition<any>>([
["title", {
exp: /contact\s+(.*)Short Description:/
}],
@@ -189,17 +254,25 @@ const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
}],
]);
-const sourceDir = path.resolve(__dirname, "source");
-const outDir = path.resolve(__dirname, "json");
-const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton");
-const successOut = "buxton.json";
-const failOut = "incomplete.json";
-const deviceKeys = Array.from(RegexMap.keys());
-
+const sourceDir = path.resolve(__dirname, "source"); // where the Word documents are assumed to be stored
+const outDir = path.resolve(__dirname, "json"); // where the JSON output of these device documents will be written
+const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton"); // where, in the server, these images will be written
+const successOut = "buxton.json"; // the JSON list representing properly formatted documents
+const failOut = "incomplete.json"; // the JSON list representing improperly formatted documents
+const deviceKeys = Array.from(FormatMap.keys()); // a way to iterate through all keys of the DeviceDocument interface
+
+/**
+ * Starts by REMOVING ALL EXISTING BUXTON RESOURCES. This might need to be
+ * changed going forward
+ * @param emitter the callback when each document is completed
+ * @param terminator the callback when the entire import is completed
+ */
export default async function executeImport(emitter: ResultCallback, terminator: TerminatorCallback) {
try {
+ // get all Word documents in the source directory
const contents = readdirSync(sourceDir);
const wordDocuments = contents.filter(file => /.*\.docx?$/.test(file)).map(file => `${sourceDir}/${file}`);
+ // removal takes place here
[outDir, imageDir].forEach(dir => {
rimraf.sync(dir);
mkdirSync(dir);
@@ -216,19 +289,28 @@ export default async function executeImport(emitter: ResultCallback, terminator:
}
}
+/**
+ * Parse every Word document in the directory, notifying any callers as needed
+ * at each iteration via the emitter.
+ * @param wordDocuments the string list of Word document names to parse
+ * @param emitter the callback when each document is completed
+ * @param terminator the callback when the entire import is completed
+ */
async function parseFiles(wordDocuments: string[], emitter: ResultCallback, terminator: TerminatorCallback): Promise<DeviceDocument[]> {
+ // execute parent-most parse function
const results: AnalysisResult[] = [];
for (const filePath of wordDocuments) {
- const fileName = path.basename(filePath).replace("Bill_Notes_", "");
+ const fileName = path.basename(filePath).replace("Bill_Notes_", ""); // not strictly needed, but cleaner
console.log(cyan(`\nExtracting contents from ${fileName}...`));
const result = analyze(fileName, await extractFileContents(filePath));
emitter(result);
results.push(result);
}
+ // collect information about errors and successes
const masterDevices: DeviceDocument[] = [];
const masterErrors: { [key: string]: string }[] = [];
- results.forEach(({ device, errors }) => {
+ results.forEach(({ device, invalid: errors }) => {
if (device) {
masterDevices.push(device);
} else if (errors) {
@@ -236,24 +318,48 @@ async function parseFiles(wordDocuments: string[], emitter: ResultCallback, term
}
});
+ // something went wrong, since errors and successes should sum to total inputs
const total = wordDocuments.length;
if (masterDevices.length + masterErrors.length !== total) {
throw new Error(`Encountered a ${masterDevices.length} to ${masterErrors.length} mismatch in device / error split!`);
}
+ // write the external JSON representations of this import
console.log();
await writeOutputFile(successOut, masterDevices, total, true);
await writeOutputFile(failOut, masterErrors, total, false);
console.log();
+ // notify the caller that the import has finished
terminator({ deviceCount: masterDevices.length, errorCount: masterErrors.length });
return masterDevices;
}
-const tableCellXPath = '//*[name()="w:tbl"]/*[name()="w:tr"]/*[name()="w:tc"]';
-const hyperlinkXPath = '//*[name()="Relationship" and contains(@Type, "hyperlink")]';
-
+/**
+ * XPath definitions for desired XML targets in respective hierarchies.
+ *
+ * For table cells, can be read as: "find me anything that looks like <w:tc> in XML, whose
+ * parent looks like <w:tr>, whose parent looks like <w:tbl>"
+ *
+ * <w:tbl>
+ * <w:tr>
+ * <w:tc>
+ *
+ * These are found by trial and error, and using an online XML parser / prettifier
+ * to inspect the structure, since the Node XML library does not expose the parsed
+ * structure very well for searching, say in the debug console.
+ */
+const xPaths = {
+ paragraphs: '//*[name()="w:p"]',
+ tableCells: '//*[name()="w:tbl"]/*[name()="w:tr"]/*[name()="w:tc"]',
+ hyperlinks: '//*[name()="Relationship" and contains(@Type, "hyperlink")]'
+};
+
+/**
+ * The meat of the script, images and text content are extracted here
+ * @param pathToDocument the path to the document relative to the root of the zip
+ */
async function extractFileContents(pathToDocument: string): Promise<DocumentContents> {
console.log('Extracting text...');
const zip = new StreamZip({ file: pathToDocument, storeEntries: true });
@@ -261,48 +367,68 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
// extract the body of the document and, specifically, its captions
const document = await Utilities.readAndParseXml(zip, "word/document.xml");
+ // get plain text
const body = document.root()?.text() ?? "No body found. Check the import script's XML parser.";
const captions: string[] = [];
const embeddedFileNames: string[] = [];
- const captionTargets = document.find(tableCellXPath).map(node => node.text().trim());
- const paragraphs = document.find('//*[name()="w:p"]').map(node => Utilities.correctSentences(node.text()).transformed!);
+ // preserve paragraph formatting and line breaks that would otherwise get lost in the plain text parsing
+ // of the XML hierarchy
+ const paragraphs = document.find(xPaths.paragraphs).map(node => Utilities.correctSentences(node.text()).transformed!);
const start = paragraphs.indexOf(paragraphs.find(el => /Bill Buxton[’']s Notes/.test(el))!) + 1;
const end = paragraphs.indexOf("Device Details");
const longDescription = paragraphs.slice(start, end).filter(paragraph => paragraph.length).join("\n\n");
- const { length } = captionTargets;
- strictEqual(length > 3, true, "No captions written.");
- strictEqual(length % 3 === 0, true, "Improper caption formatting.");
-
- for (let i = 3; i < captionTargets.length; i += 3) {
- const row = captionTargets.slice(i, i + 3);
+ // extract captions from the table cells
+ const tableRowsFlattened = document.find(xPaths.tableCells).map(node => node.text().trim());
+ const { length } = tableRowsFlattened;
+ const numCols = 3;
+ strictEqual(length > numCols, true, "No captions written."); // first row has the headers, not content
+ strictEqual(length % numCols === 0, true, "Improper caption formatting.");
+
+ // break the flat list of strings into groups of numColumns. Thus, each group represents
+ // a row in the table, where the first row has no text content since it's
+ // the image, the second has the file name and the third has the caption (maybe additional columns
+ // have been added or reordered since this was written, but follow the same appraoch)
+ for (let i = numCols; i < tableRowsFlattened.length; i += numCols) {
+ const row = tableRowsFlattened.slice(i, i + numCols);
embeddedFileNames.push(row[1]);
captions.push(row[2]);
}
// extract all hyperlinks embedded in the document
const rels = await Utilities.readAndParseXml(zip, "word/_rels/document.xml.rels");
- const hyperlinks = rels.find(hyperlinkXPath).map(el => el.attrs()[2].value());
+ const hyperlinks = rels.find(xPaths.hyperlinks).map(el => el.attrs()[2].value());
console.log("Text extracted.");
+ // write out the images for this document
console.log("Beginning image extraction...");
const imageData = await writeImages(zip);
console.log(`Extracted ${imageData.length} images.`);
+ // cleanup
zip.close();
return { body, longDescription, imageData, captions, embeddedFileNames, hyperlinks };
}
+// zip relative path from root expression / filter used to isolate only media assets
const imageEntry = /^word\/media\/\w+\.(jpeg|jpg|png|gif)/;
-interface Dimensions {
+/**
+ * Image dimensions and file suffix,
+ */
+interface ImageAttrs {
width: number;
height: number;
type: string;
}
+/**
+ * For each image, stream the file, get its size, check if it's an icon
+ * (if it is, ignore it)
+ * @param zip the zip instance data source
+ */
async function writeImages(zip: any): Promise<ImageData[]> {
const allEntries = Object.values<any>(zip.entries()).map(({ name }) => name);
const imageEntries = allEntries.filter(name => imageEntry.test(name));
@@ -315,8 +441,8 @@ async function writeImages(zip: any): Promise<ImageData[]> {
});
for (const mediaPath of imageEntries) {
- const { width, height, type } = await new Promise<Dimensions>(async resolve => {
- const sizeStream = (createImageSizeStream() as PassThrough).on('size', (dimensions: Dimensions) => {
+ const { width, height, type } = await new Promise<ImageAttrs>(async resolve => {
+ const sizeStream = (createImageSizeStream() as PassThrough).on('size', (dimensions: ImageAttrs) => {
readStream.destroy();
resolve(dimensions);
}).on("error", () => readStream.destroy());
@@ -324,11 +450,14 @@ async function writeImages(zip: any): Promise<ImageData[]> {
readStream.pipe(sizeStream);
});
+ // if it's not an icon, by this rough heuristic, i.e. is it not square
if (Math.abs(width - height) > 10) {
valid.push({ width, height, type, mediaPath });
}
}
+ // for each valid image, output the _o, _l, _m, and _s files
+ // THIS IS WHERE THE SCRIPT SPENDS MOST OF ITS TIME
for (const { type, width, height, mediaPath } of valid) {
const generatedFileName = `upload_${Utils.GenerateGuid()}.${type.toLowerCase()}`;
await DashUploadUtils.outputResizedImages(() => getImageStream(mediaPath), generatedFileName, imageDir);
@@ -342,6 +471,14 @@ async function writeImages(zip: any): Promise<ImageData[]> {
return imageUrls;
}
+/**
+ * Takes the results of extractFileContents, which relative to this is sort of the
+ * external media / preliminary text processing, and now tests the given file name to
+ * with those value definitions to make sure the body of the document contains all
+ * required fields, properly formatted
+ * @param fileName the file whose body to inspect
+ * @param contents the data already computed / parsed by extractFileContents
+ */
function analyze(fileName: string, contents: DocumentContents): AnalysisResult {
const { body, imageData, captions, hyperlinks, embeddedFileNames, longDescription } = contents;
const device: any = {
@@ -354,43 +491,56 @@ function analyze(fileName: string, contents: DocumentContents): AnalysisResult {
const errors: { [key: string]: string } = { fileName };
for (const key of deviceKeys) {
- const { exp, transformer, matchIndex, required } = RegexMap.get(key)!;
+ const { exp, transformer, matchIndex, required } = FormatMap.get(key)!;
const matches = exp.exec(body);
let captured: string;
- if (matches && (captured = matches[matchIndex ?? 1])) {
- captured = captured.replace(/\s{2,}/g, " ");
+ // if we matched and we got the specific match we're after
+ if (matches && (captured = matches[matchIndex ?? 1])) { // matchIndex defaults to 1
+ captured = captured.replace(/\s{2,}/g, " "); // remove excess whitespace
+ // if supplied, apply the required transformation (recall this is specified in FormatMap)
if (transformer) {
const { error, transformed } = transformer(captured);
if (error) {
+ // we hit a snag trying to transform the valid match
+ // still counts as a fundamental error
errors[key] = `__ERR__${key.toUpperCase()}__TRANSFORM__: ${error}`;
continue;
}
captured = transformed;
}
-
device[key] = captured;
} else if (required ?? true) {
+ // the field was either implicitly or explicitly required, and failed to match the definition in
+ // FormatMap
errors[key] = `ERR__${key.toUpperCase()}__: outer match ${matches === null ? "wasn't" : "was"} captured.`;
continue;
}
}
+ // print errors - this can be removed
const errorKeys = Object.keys(errors);
if (errorKeys.length > 1) {
console.log(red(`@ ${cyan(fileName.toUpperCase())}...`));
errorKeys.forEach(key => key !== "filename" && console.log(red(errors[key])));
- return { errors };
+ return { invalid: errors };
}
return { device };
}
+/**
+ * A utility function that writes the JSON results for this import out to the desired path
+ * @param relativePath where to write the JSON file
+ * @param data valid device document objects, or errors
+ * @param total used for more informative printing
+ * @param success whether or not the caller is writing the successful parses or the failures
+ */
async function writeOutputFile(relativePath: string, data: any[], total: number, success: boolean) {
console.log(yellow(`Encountered ${data.length} ${success ? "valid" : "invalid"} documents out of ${total} candidates. Writing ${relativePath}...`));
return new Promise<void>((resolve, reject) => {
const destination = path.resolve(outDir, relativePath);
- const contents = JSON.stringify(data, undefined, 4);
+ const contents = JSON.stringify(data, undefined, 4); // format the JSON
writeFile(destination, contents, err => err ? reject(err) : resolve());
});
} \ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 3f903a861..8567631cd 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -325,12 +325,7 @@ export namespace DashUploadUtils {
const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix));
await new Promise<void>(async (resolve, reject) => {
const source = streamProvider();
- let readStream: Stream;
- if (source instanceof Promise) {
- readStream = await source;
- } else {
- readStream = source;
- }
+ let readStream: Stream = source instanceof Promise ? await source : source;
if (resizer) {
readStream = readStream.pipe(resizer.withMetadata());
}
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index 947aa4289..be895c4bc 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -10,7 +10,6 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
import { logPort } from "../ActionUtilities";
import { timeMap } from "../ApiManagers/UserManager";
import { green } from "colors";
-import { serverPathToFile, Directory } from "../ApiManagers/UploadManager";
import { networkInterfaces } from "os";
import executeImport from "../../scraping/buxton/final/BuxtonImporter";
@@ -111,6 +110,12 @@ export namespace WebSocket {
Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+
+ /**
+ * Whenever we receive the go-ahead, invoke the import script and pass in
+ * as an emitter and a terminator the functions that simply broadcast a result
+ * or indicate termination to the client via the web socket
+ */
Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => {
executeImport(
deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError),
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index b5a44a8bc..bf99f449f 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,5 +1,6 @@
import { action, computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
+import { Utils } from "../../../Utils";
import { DocServer } from "../../../client/DocServer";
import { Docs, DocumentOptions } from "../../../client/documents/Documents";
import { UndoManager } from "../../../client/util/UndoManager";
@@ -7,8 +8,7 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
-import { Cast, PromiseValue, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
+import { Cast, PromiseValue, StrCast, NumCast } from "../../../new_fields/Types";
import { nullAudio, ImageField } from "../../../new_fields/URLField";
import { DragManager } from "../../../client/util/DragManager";
import { InkingControl } from "../../../client/views/InkingControl";
@@ -17,9 +17,10 @@ import { CollectionViewType } from "../../../client/views/collections/Collection
import { makeTemplate } from "../../../client/util/DropConverter";
import { RichTextField } from "../../../new_fields/RichTextField";
import { PrefetchProxy } from "../../../new_fields/Proxy";
-import { FormattedTextBox } from "../../../client/views/nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../../client/views/nodes/formattedText/FormattedTextBox";
import { MainView } from "../../../client/views/MainView";
import { DocumentType } from "../../../client/documents/DocumentTypes";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
export class CurrentUserUtils {
private static curr_id: string;
@@ -48,7 +49,7 @@ export class CurrentUserUtils {
);
queryTemplate.isTemplateDoc = makeTemplate(queryTemplate);
doc["template-button-query"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'),
dragFactory: new PrefetchProxy(queryTemplate) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle"
});
@@ -64,19 +65,21 @@ export class CurrentUserUtils {
);
slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
doc["template-button-slides"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'),
dragFactory: new PrefetchProxy(slideTemplate) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card"
});
}
if (doc["template-button-description"] === undefined) {
- const descriptionTemplate = Docs.Create.TextDocument("", { title: "text", _height: 100, _showTitle: "title" });
- Doc.GetProto(descriptionTemplate).layout = FormattedTextBox.LayoutString("description");
+ const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created
+ Doc.GetProto(descriptionTemplate).layout =
+ "<div><FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" +
+ "<FormattedTextBox {...props} height='calc(100% - {this._headerHeight||75}px)' fieldKey={'text'}/></div>";
descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
doc["template-button-description"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'),
dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize"
});
@@ -85,10 +88,14 @@ export class CurrentUserUtils {
if (doc["template-button-detail"] === undefined) {
const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create;
- const carousel = CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" });
+ const openInTarget = ScriptField.MakeScript("openOnRight(self.doubleClickView)");
+ const carousel = CarouselDocument([], {
+ title: "data", _height: 350, _itemIndex: 0, "_carousel-caption-xMargin": 10, "_carousel-caption-yMargin": 10,
+ onChildDoubleClick: openInTarget, backgroundColor: "#9b9b9b3F"
+ });
const details = TextDocument("", { title: "details", _height: 350, _autoHeight: true });
- const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 50, _autoHeight: true });
+ const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 100, _autoHeight: true });
const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 350, _autoHeight: true });
const buxtonFieldKeys = ["year", "originalPrice", "degreesOfFreedom", "company", "attribute", "primaryKey", "secondaryKey", "dimensions"];
@@ -108,15 +115,21 @@ export class CurrentUserUtils {
const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: 12 };
const descriptionWrapperOpts = { title: "descriptions", _height: 300, columnWidth: -1, treeViewHideTitle: true, _pivotField: "title" };
- const descriptionWrapper = MasonryDocument([short, long], { ...shared, ...descriptionWrapperOpts });
- const detailView = Docs.Create.StackingDocument([carousel, details, descriptionWrapper], { ...shared, ...detailViewOpts });
+ const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts });
+ descriptionWrapper.sectionHeaders = new List<SchemaHeaderField>([
+ new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false),
+ new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true),
+ new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true),
+ ]);
+ const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts });
detailView.isTemplateDoc = makeTemplate(detailView);
+ details.title = "Details";
short.title = "A Short Description";
long.title = "Long Description";
doc["template-button-detail"] = CurrentUserUtils.ficon({
- onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'),
dragFactory: new PrefetchProxy(detailView) as any as Doc,
removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "window-maximize"
});
@@ -125,7 +138,7 @@ export class CurrentUserUtils {
if (doc["template-buttons"] === undefined) {
doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc,
doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc], {
- title: "Compound Item Creators", _xMargin: 0, _showTitle: "title",
+ title: "Document Prototypes", _xMargin: 0, _showTitle: "title",
_autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
}));
@@ -175,9 +188,13 @@ export class CurrentUserUtils {
doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc],
{ title: "Note Layouts", _height: 75 }));
} else {
- const noteTypes = Cast(doc["template-notes"], Doc, null);
- DocListCastAsync(noteTypes).then(list => noteTypes.data = new List<Doc>([doc["template-note-Note"] as any as Doc,
- doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc]));
+ const curNoteTypes = Cast(doc["template-notes"], Doc, null);
+ const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc,
+ doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc];
+ DocListCastAsync(curNoteTypes.data).then(async curNotes => {
+ await Promise.all(curNotes!);
+ requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
+ });
}
return doc["template-notes"] as Doc;
@@ -200,28 +217,29 @@ export class CurrentUserUtils {
static setupDefaultIconTemplates(doc: Doc) {
if (doc["template-icon-view"] === undefined) {
const iconView = Docs.Create.TextDocument("", {
- title: "icon", _width: 150, _height: 30, isTemplateDoc: true,
- onClick: ScriptField.MakeScript("deiconifyView(self)")
+ title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
});
Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
iconView.isTemplateDoc = makeTemplate(iconView);
doc["template-icon-view"] = new PrefetchProxy(iconView);
}
if (doc["template-icon-view-rtf"] === undefined) {
- const iconRtfView = Docs.Create.LabelDocument({ title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(self)") });
+ const iconRtfView = Docs.Create.LabelDocument({
+ title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset",
+ _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+ });
iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF);
doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView);
}
if (doc["template-icon-view-img"] === undefined) {
const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", {
- title: "data", _width: 50, isTemplateDoc: true,
- onClick: ScriptField.MakeScript("deiconifyView(self)")
+ title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
});
iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG);
doc["template-icon-view-img"] = new PrefetchProxy(iconImageView);
}
if (doc["template-icon-view-col"] === undefined) {
- const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onClick: ScriptField.MakeScript("deiconifyView(self)") });
+ const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") });
iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL);
doc["template-icon-view-col"] = new PrefetchProxy(iconColView);
}
@@ -242,15 +260,23 @@ export class CurrentUserUtils {
}[] {
if (doc.emptyPresentation === undefined) {
doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
- { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
+ { title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
}
if (doc.emptyCollection === undefined) {
doc.emptyCollection = Docs.Create.FreeformDocument([],
{ _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
}
+ if (doc.emptyDocHolder === undefined) {
+ doc.emptyDocHolder = Docs.Create.DocumentDocument(
+ ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
+ { _width: 250, _height: 250, title: "container" });
+ }
+ if (doc.emptyWebpage === undefined) {
+ doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600 })
+ }
return [
{ title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
- { title: "Drag a web page", label: "Web", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("", { title: "New Webpage" })' },
+ { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
{ title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
{ title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
{ title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
@@ -267,7 +293,7 @@ export class CurrentUserUtils {
// { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
// { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
// { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
- { title: "Drag a document previewer", label: "Prev", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
+ { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
{ title: "Drag a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
];
@@ -291,7 +317,7 @@ export class CurrentUserUtils {
title: data.title,
label: data.label,
ignoreClick: data.ignoreClick,
- dropAction: data.click ? "copy" : undefined,
+ dropAction: "copy",
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined,
@@ -470,7 +496,7 @@ export class CurrentUserUtils {
_width: 50, _height: 25, title: "Library", _fontSize: 10,
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], {
- title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "move", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true
+ title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "move", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true
})) as any as Doc,
targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
@@ -561,14 +587,9 @@ export class CurrentUserUtils {
// the initial presentation Doc to use
static setupDefaultPresentation(doc: Doc) {
- if (doc["template-presentation"] === undefined) {
- doc["template-presentation"] = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
- title: "pres element template", backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data"
- }));
- }
if (doc.activePresentation === undefined) {
doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), {
- title: "Presentation", _viewType: CollectionViewType.Stacking,
+ title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias",
_LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0"
});
}
@@ -581,25 +602,37 @@ export class CurrentUserUtils {
}
static setupClickEditorTemplates(doc: Doc) {
- if (doc.childClickFuncs === undefined) {
+ if (doc["clickFuncs-child"] === undefined) {
const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
- "docCast(thisContainer.target).then((target) => { target && docCast(this.source).then((source) => { target.proto.data = new List([source || this]); } ); } )",
- { target: Doc.name }), { title: "On Child Clicked (open in target)", _width: 300, _height: 200 });
+ "docCast(thisContainer.target).then((target) => {" +
+ " target && docCast(this.source).then((source) => { " +
+ " target.proto.data = new List([source || this]); " +
+ " }); " +
+ "})",
+ { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" });
+
+ const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
+ "openOnRight(self.doubleClickView)",
+ { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" });
- doc.childClickFuncs = Docs.Create.TreeDocument([openInTarget], { title: "on Child Click function templates" });
+ doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" });
}
// this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved.
- PromiseValue(Cast(doc.childClickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
+ PromiseValue(Cast(doc["clickFuncs-child"], Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
if (doc.clickFuncs === undefined) {
const onClick = Docs.Create.ScriptingDocument(undefined, {
title: "onClick", "onClick-rawScript": "console.log('click')",
isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200
}, "onClick");
+ const onDoubleClick = Docs.Create.ScriptingDocument(undefined, {
+ title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')",
+ isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200
+ }, "onDoubleClick");
const onCheckedClick = Docs.Create.ScriptingDocument(undefined, {
title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200
}, "onCheckedClick");
- doc.clickFuncs = Docs.Create.TreeDocument([onClick, onCheckedClick], { title: "onClick funcs" });
+ doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" });
}
PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
@@ -610,6 +643,9 @@ export class CurrentUserUtils {
new InkingControl();
doc.title = Doc.CurrentUserEmail;
doc.activePen = doc;
+ doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); //
+ doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); //
+ Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing